Beispiel #1
0
    def stop(self) -> Optional[float]:
        """
        Stops tracking the experiment
        :return: CO2 emissions in kgs
        """
        if self._start_time is None:
            logger.error("Need to first start the tracker")
            return None

        if self._scheduler:
            self._scheduler.stop()

        # Run to calculate the power used from last
        # scheduled measurement to shutdown
        self._measure_power_and_energy()

        emissions_data = self._prepare_emissions_data()

        for persistence in self.persistence_objs:
            if isinstance(persistence, CodeCarbonAPIOutput):
                emissions_data = self._prepare_emissions_data(delta=True)

            persistence.out(emissions_data)

        self.final_emissions_data = emissions_data
        self.final_emissions = emissions_data.emissions
        return emissions_data.emissions
Beispiel #2
0
    def _fetch_rapl_files(self):
        """
        Fetches RAPL files from the RAPL directory
        """

        # consider files like `intel-rapl:$i`
        files = list(filter(lambda x: ":" in x,
                            os.listdir(self._lin_rapl_dir)))

        i = 0
        for file in files:
            path = os.path.join(self._lin_rapl_dir, file, "name")
            with open(path) as f:
                name = f.read().strip()
                # Fake the name used by Power Gadget
                if "package" in name:
                    name = f"Processor Energy Delta_{i}(kWh)"
                    i += 1
                rapl_file = os.path.join(self._lin_rapl_dir, file, "energy_uj")
                try:
                    # Try to read the file to be sure we can
                    with open(rapl_file, "r") as f:
                        _ = float(f.read())
                    self._rapl_files.append(RAPLFile(name=name,
                                                     path=rapl_file))
                    logger.debug(
                        f"We will read Intel RAPL files at {rapl_file}")
                except PermissionError as e:
                    logger.error(
                        "Unable to read Intel RAPL files for CPU power, we will use a constant for your CPU power."
                        +
                        " Please view https://github.com/mlco2/codecarbon/issues/244"
                        + f" for workarounds : {e}")
        return
Beispiel #3
0
 def _log_error(self, url, payload, response):
     logger.error(
         f"ApiClient Error when calling the API on {url} with : {json.dumps(payload)}"
     )
     logger.error(
         f"ApiClient API return http code {response.status_code} and answer : {response.text}"
     )
Beispiel #4
0
    def get_private_infra_emissions(self, energy: Energy,
                                    geo: GeoMetadata) -> float:
        """
        Computes emissions for private infra
        :param energy: Mean power consumption of the process (kWh)
        :param geo: Country and region metadata
        :return: CO2 emissions in kg
        """
        if self._co2_signal_api_token:
            try:
                return co2_signal.get_emissions(energy, geo,
                                                self._co2_signal_api_token)
            except Exception as e:
                logger.error("co2_signal.get_emissions: " + str(e) +
                             " >>> Using CodeCarbon's data.")

        compute_with_regional_data: bool = (geo.region is not None) and (
            geo.country_iso_code.upper() in ["USA", "CAN"])

        if compute_with_regional_data:
            try:
                return self.get_region_emissions(energy, geo)
            except Exception as e:
                logger.error(e, exc_info=True)
                logger.warning("Regional emissions retrieval failed." +
                               " Falling back on country emissions.")
        return self.get_country_emissions(energy, geo)
Beispiel #5
0
 def out(self, data: EmissionsData):
     try:
         payload = dataclasses.asdict(data)
         payload["user"] = getpass.getuser()
         resp = requests.post(self.endpoint_url, json=payload, timeout=10)
         if resp.status_code != 201:
             logger.warning(
                 "HTTP Output returned an unexpected status code: ",
                 resp,
             )
     except Exception as e:
         logger.error(e, exc_info=True)
Beispiel #6
0
 def add_emission(self, carbon_emission: dict):
     assert self.experiment_id is not None
     self._previous_call = time.time()
     if self.run_id is None:
         # TODO : raise an Exception ?
         logger.debug(
             "ApiClient.add_emission need a run_id : the initial call may "
             + "have failed. Retrying..."
         )
         self._create_run(self.experiment_id)
         if self.run_id is None:
             logger.error(
                 "ApiClient.add_emission still no run_id, aborting for this time !"
             )
         return False
     if carbon_emission["duration"] < 1:
         logger.warning(
             "ApiClient : emissions not sent because of a duration smaller than 1."
         )
         return False
     emission = EmissionCreate(
         timestamp=get_datetime_with_timezone(),
         run_id=self.run_id,
         duration=int(carbon_emission["duration"]),
         emissions_sum=carbon_emission["emissions"],
         emissions_rate=carbon_emission["emissions_rate"],
         cpu_power=carbon_emission["cpu_power"],
         gpu_power=carbon_emission["gpu_power"],
         ram_power=carbon_emission["ram_power"],
         cpu_energy=carbon_emission["cpu_energy"],
         gpu_energy=carbon_emission["gpu_energy"],
         ram_energy=carbon_emission["ram_energy"],
         energy_consumed=carbon_emission["energy_consumed"],
     )
     try:
         payload = dataclasses.asdict(emission)
         url = self.url + "/emission"
         r = requests.post(url=url, json=payload, timeout=2)
         if r.status_code != 201:
             self._log_error(url, payload, r)
             return False
         logger.debug(f"ApiClient - Successful upload emission {payload} to {url}")
     except Exception as e:
         logger.error(e, exc_info=True)
         return False
     return True
Beispiel #7
0
    def _global_energy_mix_to_emissions_rate(
            energy_mix: Dict) -> EmissionsPerKWh:
        """
        Convert a mix of electricity sources into emissions per kWh.
        :param energy_mix: A dictionary that breaks down the electricity produced into
            energy sources, with a total value. Format will vary, but must have keys for "total_TWh"
        :return: an EmissionsPerKwh object representing the average emissions rate
            in Kgs.CO2 / kWh
        """
        # If we have the chance to have the carbon intensity for this country
        if energy_mix.get("carbon_intensity"):
            return EmissionsPerKWh.from_g_per_kWh(
                energy_mix.get("carbon_intensity"))

        # Else we compute it from the energy mix.
        # Read carbon_intensity from the json data file.
        carbon_intensity_per_source = (
            DataSource().get_carbon_intensity_per_source_data())
        carbon_intensity = 0
        energy_sum = energy_mix["total_TWh"]
        energy_sum_computed = 0
        # Iterate through each source of energy in the country
        for energy_type, energy_per_year in energy_mix.items():
            if "_TWh" in energy_type:
                # Compute the carbon intensity ratio of this source for this country
                carbon_intensity_for_type = carbon_intensity_per_source.get(
                    energy_type[:-len("_TWh")])
                if carbon_intensity_for_type:  # to ignore "total_TWh"
                    carbon_intensity += (energy_per_year / energy_sum
                                         ) * carbon_intensity_for_type
                    energy_sum_computed += energy_per_year

        # Sanity check
        if energy_sum_computed != energy_sum:
            logger.error(
                f"We find {energy_sum_computed} TWh instead of {energy_sum} TWh for {energy_mix.get('official_name_en')}, using world average."
            )
            return EmissionsPerKWh.from_g_per_kWh(
                carbon_intensity_per_source.get("world_average"))

        return EmissionsPerKWh.from_g_per_kWh(carbon_intensity)
Beispiel #8
0
 def _create_run(self, experiment_id):
     """
     Create the experiment for project_id
     # TODO : Allow to give an existing experiment_id
     """
     if self.experiment_id is None:
         # TODO : raise an Exception ?
         logger.error("ApiClient FATAL The API _create_run needs an experiment_id !")
         return None
     try:
         run = RunCreate(
             timestamp=get_datetime_with_timezone(),
             experiment_id=experiment_id,
             os=self.conf.get("os"),
             python_version=self.conf.get("python_version"),
             cpu_count=self.conf.get("cpu_count"),
             cpu_model=self.conf.get("cpu_model"),
             gpu_count=self.conf.get("gpu_count"),
             gpu_model=self.conf.get("gpu_model"),
             longitude=self.conf.get("longitude"),
             latitude=self.conf.get("latitude"),
             region=self.conf.get("region"),
             provider=self.conf.get("provider"),
             ram_total_size=self.conf.get("ram_total_size"),
             tracking_mode=self.conf.get("tracking_mode"),
         )
         payload = dataclasses.asdict(run)
         url = self.url + "/run"
         r = requests.post(url=url, json=payload, timeout=2)
         if r.status_code != 201:
             self._log_error(url, payload, r)
             return None
         self.run_id = r.json()["id"]
         logger.info(
             "ApiClient Successfully registered your run on the API.\n\n"
             + f"Run ID: {self.run_id}\n"
             + f"Experiment ID: {self.experiment_id}\n"
         )
         return self.run_id
     except Exception as e:
         logger.error(e, exc_info=True)
Beispiel #9
0
    def flush(self) -> Optional[float]:
        """
        Write emission to disk or call the API depending on the configuration
        but keep running the experiment.
        :return: CO2 emissions in kgs
        """
        if self._start_time is None:
            logger.error("Need to first start the tracker")
            return None

        # Run to calculate the power used from last
        # scheduled measurement to shutdown
        self._measure_power_and_energy()

        emissions_data = self._prepare_emissions_data()
        for persistence in self.persistence_objs:
            if isinstance(persistence, CodeCarbonAPIOutput):
                emissions_data = self._prepare_emissions_data(delta=True)
            persistence.out(emissions_data)

        return emissions_data.emissions
Beispiel #10
0
    def __init__(
        self,
        *args,
        country_iso_code: Optional[str] = _sentinel,
        region: Optional[str] = _sentinel,
        cloud_provider: Optional[str] = _sentinel,
        cloud_region: Optional[str] = _sentinel,
        country_2letter_iso_code: Optional[str] = _sentinel,
        **kwargs,
    ):
        """
        :param country_iso_code: 3 letter ISO Code of the country where the
                                 experiment is being run
        :param region: The provincial region, for example, California in the US.
                       Currently, this only affects calculations for the United States
                       and Canada
        :param cloud_provider: The cloud provider specified for estimating emissions
                               intensity, defaults to None.
                               See https://github.com/mlco2/codecarbon/
                                        blob/master/codecarbon/data/cloud/impact.csv
                               for a list of cloud providers
        :param cloud_region: The region of the cloud data center, defaults to None.
                             See https://github.com/mlco2/codecarbon/
                                        blob/master/codecarbon/data/cloud/impact.csv
                             for a list of cloud regions.
        :param country_2letter_iso_code: For use with the CO2Signal emissions API.
                                         See http://api.electricitymap.org/v3/zones for
                                         a list of codes and their corresponding
                                         locations.
        """
        self._external_conf = get_hierarchical_config()
        self._set_from_conf(cloud_provider, "cloud_provider")
        self._set_from_conf(cloud_region, "cloud_region")
        self._set_from_conf(country_2letter_iso_code,
                            "country_2letter_iso_code")
        self._set_from_conf(country_iso_code, "country_iso_code")
        self._set_from_conf(region, "region")

        logger.info("offline tracker init")

        if self._region is not None:
            assert isinstance(self._region, str)
            self._region: str = self._region.lower()

        if self._cloud_provider:
            if self._cloud_region is None:
                logger.error("Cloud Region must be provided " +
                             " if cloud provider is set")

            df = DataSource().get_cloud_emissions_data()
            if (len(df.loc[(df["provider"] == self._cloud_provider)
                           & (df["region"] == self._cloud_region)]) == 0):
                logger.error("Cloud Provider/Region "
                             f"{self._cloud_provider} {self._cloud_region} "
                             "not found in cloud emissions data.")
        if self._country_iso_code:
            try:
                self._country_name: str = DataSource(
                ).get_global_energy_mix_data()[
                    self._country_iso_code]["country_name"]
            except KeyError as e:
                logger.error("Does not support country" +
                             f" with ISO code {self._country_iso_code} "
                             f"Exception occurred {e}")

        if self._country_2letter_iso_code:
            assert isinstance(self._country_2letter_iso_code, str)
            self._country_2letter_iso_code: str = self._country_2letter_iso_code.upper(
            )

        super().__init__(*args, **kwargs)
Beispiel #11
0
    def _measure_power_and_energy(self) -> None:
        """
        A function that is periodically run by the `BackgroundScheduler`
        every `self._measure_power_secs` seconds.
        :return: None
        """
        last_duration = time.time() - self._last_measured_time

        warning_duration = self._measure_power_secs * 3
        if last_duration > warning_duration:
            warn_msg = ("Background scheduler didn't run for a long period" +
                        " (%ds), results might be inaccurate")
            logger.warning(warn_msg, last_duration)

        for hardware in self._hardware:
            h_time = time.time()
            # Compute last_duration again for more accuracy
            last_duration = time.time() - self._last_measured_time
            power, energy = hardware.measure_power_and_energy(
                last_duration=last_duration)
            self._total_energy += energy
            if isinstance(hardware, CPU):
                self._total_cpu_energy += energy
                self._cpu_power = power
                logger.info(
                    f"Energy consumed for all CPUs : {self._total_cpu_energy.kWh:.6f} kWh"
                    + f". All CPUs Power : {self._cpu_power.W} W")
            elif isinstance(hardware, GPU):
                self._total_gpu_energy += energy
                self._gpu_power = power
                logger.info(
                    f"Energy consumed for all GPUs : {self._total_gpu_energy.kWh:.6f} kWh"
                    + f". All GPUs Power : {self._gpu_power.W} W")
            elif isinstance(hardware, RAM):
                self._total_ram_energy += energy
                self._ram_power = power
                logger.info(
                    f"Energy consumed for RAM : {self._total_ram_energy.kWh:.6f} kWh"
                    + f". RAM Power : {self._ram_power.W} W")
            else:
                logger.error(
                    f"Unknown hardware type: {hardware} ({type(hardware)})")
            h_time = time.time() - h_time
            logger.debug(
                f"{hardware.__class__.__name__} : {hardware.total_power().W:,.2f} "
                +
                f"W during {last_duration:,.2f} s [measurement time: {h_time:,.4f}]"
            )
        logger.info(
            f"{self._total_energy.kWh:.6f} kWh of electricity used since the begining."
        )
        self._last_measured_time = time.time()
        self._measure_occurrence += 1
        if self._cc_api__out is not None and self._api_call_interval != -1:
            if self._measure_occurrence >= self._api_call_interval:
                emissions = self._prepare_emissions_data(delta=True)
                logger.info(
                    f"{emissions.emissions_rate:.6f} g.CO2eq/s mean an estimation of "
                    +
                    f"{emissions.emissions_rate*3600*24*365/1000:,} kg.CO2eq/year"
                )
                self._cc_api__out.out(emissions)
                self._measure_occurrence = 0
        logger.debug(
            f"last_duration={last_duration}\n------------------------")
Beispiel #12
0
 def out(self, data: EmissionsData):
     try:
         payload = dataclasses.asdict(data)
         self.logger.log_struct(payload, severity=self.logging_severity)
     except Exception as e:
         logger.error(e, exc_info=True)
Beispiel #13
0
 def out(self, data: EmissionsData):
     try:
         payload = dataclasses.asdict(data)
         self.logger.log(self.logging_severity, msg=json.dumps(payload))
     except Exception as e:
         logger.error(e, exc_info=True)
Beispiel #14
0
 def out(self, data: EmissionsData):
     try:
         self.api.add_emission(dataclasses.asdict(data))
     except Exception as e:
         logger.error(e, exc_info=True)