-
-
Notifications
You must be signed in to change notification settings - Fork 4
Add Ecologits-based LLM inference energy estimation for AWS Bedrock #163
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
jnioche
merged 23 commits into
DigitalPebble:main
from
dpol1:feature/bedrock-ai-estimates
Mar 22, 2026
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
bfb00d6
Add ecologits static data for AWS Bedrock models
dpol1 e10aa86
Add core EcoLogits coefficient loader and cache
dpol1 3339f3b
Add new inner class to handle the coefficients
dpol1 67276b7
Rename core class to EcoLogits based on feedback
dpol1 7bd6662
Merge branch 'main' into feature/bedrock-ai-estimates
jnioche 8b02d13
Add BedrockEcoLogits module skeleton
dpol1 1c0f7a0
Merge branch 'main' into feature/bedrock-ai-estimates
dpol1 b02e773
Implement enrichment logic for BedrockEcoLogits
dpol1 e41ae8d
Add package-info for ecologits module
dpol1 d8bf3c1
Refine Bedrock energy estimation with usage type detection
dpol1 e2a7509
Add unit tests for BedrockEcoLogits
dpol1 60392bb
Add unit tests for EcoLogits
dpol1 568bf42
Add BedrockEcoLogits module to default-config.json pipeline
dpol1 3b767f6
Add commercial model names to Bedrock energy estimates
dpol1 d524102
docs: add EcoLogits module documentation
dpol1 3ec1a14
Merge branch 'main' into feature/bedrock-ai-estimates
dpol1 5c1dda4
docs: refine module documentation
dpol1 3c6d13e
Refine BedrockEcoLogits logic and improve logging
dpol1 6f405df
Update Bedrock energy and emission coefficients
dpol1 de90118
Simplify product map extraction in BedrockEcoLogits
dpol1 d7db7b6
Add model aliases support and remove duplicate coefficients
dpol1 741a68e
Simplify column value extraction in BedrockEcoLogitsTest
dpol1 18f1ce6
docs: add note on static batch size assumption
dpol1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
146 changes: 146 additions & 0 deletions
146
src/main/java/com/digitalpebble/spruce/modules/ecologits/BedrockEcoLogits.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package com.digitalpebble.spruce.modules.ecologits; | ||
|
|
||
| import com.digitalpebble.spruce.Column; | ||
| import com.digitalpebble.spruce.EnrichmentModule; | ||
| import org.apache.spark.sql.Row; | ||
|
|
||
| import java.util.Map; | ||
|
|
||
| import static com.digitalpebble.spruce.CURColumn.*; | ||
| import static com.digitalpebble.spruce.SpruceColumn.*; | ||
|
|
||
| /** | ||
| * Enrichment module estimating energy consumption and embodied emissions | ||
| * for LLM inference on AWS Bedrock. | ||
| * <p> | ||
| * It extracts the model name from the CUR {@code product} map, determines token | ||
| * types (input/output) via {@code line_item_usage_type}, and retrieves coefficients | ||
| * using {@link EcoLogits}. If the usage type is ambiguous, it falls back to | ||
| * a configurable {@code input_token_ratio} (default 0.5). | ||
| * <p> | ||
| * Usage amounts are normalized to individual tokens based on the {@code pricing_unit} | ||
| * before applying the per-1K-token coefficients. | ||
| */ | ||
| public class BedrockEcoLogits implements EnrichmentModule { | ||
|
|
||
| private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(BedrockEcoLogits.class); | ||
|
|
||
| private EcoLogits impacts; | ||
|
|
||
| private double inputTokenRatio = 0.5; | ||
|
|
||
| @Override | ||
| public void init(Map<String, Object> params) { | ||
| impacts = new EcoLogits(); | ||
| impacts.load(); | ||
|
|
||
| if (params != null) { | ||
| Number ratio = (Number) params.get("input_token_ratio"); | ||
| if (ratio != null) { | ||
| inputTokenRatio = ratio.doubleValue(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public Column[] columnsNeeded() { | ||
| return new Column[]{LINE_ITEM_PRODUCT_CODE, PRODUCT, USAGE_AMOUNT, PRICING_UNIT, LINE_ITEM_USAGE_TYPE}; | ||
| } | ||
|
|
||
| @Override | ||
| public Column[] columnsAdded() { | ||
| return new Column[]{ENERGY_USED, EMBODIED_EMISSIONS}; | ||
| } | ||
|
|
||
| @Override | ||
| public void enrich(Row row, Map<Column, Object> enrichedValues) { | ||
| String productCode = LINE_ITEM_PRODUCT_CODE.getString(row); | ||
| if (!"AmazonBedrock".equals(productCode)) { | ||
| return; | ||
| } | ||
|
|
||
| int productIndex = PRODUCT.resolveIndex(row); | ||
| Map<Object, Object> productMap = row.getJavaMap(productIndex); | ||
| if (productMap == null) { | ||
| return; | ||
| } | ||
|
|
||
| String modelId = (String) productMap.get("model"); | ||
| if (modelId == null || modelId.isEmpty()) { | ||
| LOG.warn("BedrockEcoLogits: model key missing or empty in product map"); | ||
| return; | ||
| } | ||
|
|
||
| EcoLogits.ModelImpacts modelImpacts = impacts.getImpacts(modelId); | ||
| if (modelImpacts == null) { | ||
| LOG.warn("BedrockEcoLogits: no EcoLogits coefficients found for model '{}'", modelId); | ||
| return; | ||
|
dpol1 marked this conversation as resolved.
|
||
| } | ||
|
|
||
| if (USAGE_AMOUNT.isNullAt(row)) { | ||
| return; | ||
| } | ||
| double usageAmount = USAGE_AMOUNT.getDouble(row); | ||
| if (usageAmount <= 0) { | ||
| return; | ||
| } | ||
|
|
||
| double tokenMultiplier = parseTokenMultiplier(PRICING_UNIT.getString(row)); | ||
| double totalTokens = usageAmount * tokenMultiplier; | ||
|
|
||
| // Check Input vs Output based on line_item_usage_type | ||
| boolean isInput = false; | ||
| boolean isOutput = false; | ||
|
|
||
| String usageType = LINE_ITEM_USAGE_TYPE.getString(row); | ||
| if (usageType != null) { | ||
| String lower = usageType.toLowerCase(); | ||
| isInput = lower.contains("input"); | ||
| isOutput = lower.contains("output"); | ||
| } | ||
|
|
||
| double energyKwh; | ||
| if (isInput && !isOutput) { | ||
| energyKwh = (totalTokens / 1_000.0) * modelImpacts.getEnergyKwhPer1kInputTokens(); | ||
| } else if (isOutput && !isInput) { | ||
| energyKwh = (totalTokens / 1_000.0) * modelImpacts.getEnergyKwhPer1kOutputTokens(); | ||
| } else { | ||
| // Fallback if CUR does not specify, or if the string is ambiguous (contains both) | ||
| double inputTokens = totalTokens * inputTokenRatio; | ||
| double outputTokens = totalTokens * (1.0 - inputTokenRatio); | ||
| energyKwh = (inputTokens / 1_000.0) * modelImpacts.getEnergyKwhPer1kInputTokens() | ||
| + (outputTokens / 1_000.0) * modelImpacts.getEnergyKwhPer1kOutputTokens(); | ||
| } | ||
|
|
||
| double embodiedEmissions = (totalTokens / 1_000.0) * modelImpacts.getEmbodiedCo2eGPer1kTokens(); | ||
|
|
||
| enrichedValues.put(ENERGY_USED, energyKwh); | ||
| enrichedValues.put(EMBODIED_EMISSIONS, embodiedEmissions); | ||
|
|
||
| LOG.debug("Bedrock model={} tokens={} energy_kwh={} embodied_g={}", modelId, totalTokens, energyKwh, embodiedEmissions); | ||
| } | ||
|
|
||
| /** | ||
| * Parses the CUR {@code pricing_unit} to determine how many individual tokens | ||
| * one usage-amount unit represents. (e.g. "1M tokens" → 1_000_000) | ||
| */ | ||
| static double parseTokenMultiplier(String pricingUnit) { | ||
| if (pricingUnit == null || pricingUnit.isBlank()) { | ||
| return 1.0; | ||
| } | ||
| String lower = pricingUnit.trim().toLowerCase(); | ||
| if (!lower.contains("token")) { | ||
| return 1.0; | ||
| } | ||
| if (lower.startsWith("1m")) { | ||
| return 1_000_000.0; | ||
| } | ||
| if (lower.startsWith("1k")) { | ||
| return 1_000.0; | ||
| } | ||
| return 1.0; | ||
| } | ||
|
|
||
| } | ||
98 changes: 98 additions & 0 deletions
98
src/main/java/com/digitalpebble/spruce/modules/ecologits/EcoLogits.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package com.digitalpebble.spruce.modules.ecologits; | ||
|
|
||
| import com.digitalpebble.spruce.Utils; | ||
|
|
||
| import java.io.IOException; | ||
| import java.io.Serializable; | ||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
| import java.util.Set; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
|
|
||
| /** | ||
| * Cloud-agnostic lookup for LLM inference energy and embodied emissions coefficients. | ||
| * <p> | ||
| * Loads a static JSON file (extracted from EcoLogits data) that maps model IDs to | ||
| * energy-per-1K-token and embodied-emissions-per-1K-token values. | ||
| */ | ||
| public class EcoLogits implements Serializable { | ||
|
|
||
| private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(EcoLogits.class); | ||
|
|
||
| private static final String DEFAULT_RESOURCE = "ecologits/bedrock.json"; | ||
|
|
||
| private final Map<String, ModelImpacts> modelsMap = new HashMap<>(); | ||
| private final Set<String> unknownModels = ConcurrentHashMap.newKeySet(); | ||
|
|
||
| /** | ||
| * Loads the model coefficients from the bundled JSON resource into the memory map. | ||
| * This should typically be called once during the initialization phase. | ||
| * | ||
| * @throws RuntimeException if the resource file cannot be read, parsed, or found. | ||
| */ | ||
| @SuppressWarnings("unchecked") | ||
| public void load() { | ||
| try { | ||
| Map<String, Object> raw = Utils.loadJSONResources(DEFAULT_RESOURCE); | ||
| for (Map.Entry<String, Object> entry : raw.entrySet()) { | ||
| Map<String, Object> values = (Map<String, Object>) entry.getValue(); | ||
| double inputEnergy = ((Number) values.get("energy_kwh_per_1k_input_tokens")).doubleValue(); | ||
| double outputEnergy = ((Number) values.get("energy_kwh_per_1k_output_tokens")).doubleValue(); | ||
| double embodied = ((Number) values.get("embodied_co2e_g_per_1k_tokens")).doubleValue(); | ||
| ModelImpacts impacts = new ModelImpacts(inputEnergy, outputEnergy, embodied); | ||
| modelsMap.put(entry.getKey(), impacts); | ||
|
|
||
| // register aliases pointing to the same ModelImpacts instance | ||
| if (values.containsKey("aliases")) { | ||
| for (String alias : (java.util.List<String>) values.get("aliases")) { | ||
| modelsMap.put(alias, impacts); | ||
| } | ||
| } | ||
| } | ||
| LOG.info("Loaded {} LLM model coefficients from {}", modelsMap.size(), DEFAULT_RESOURCE); | ||
| } catch (IOException e) { | ||
| throw new RuntimeException("Failed to load LLM model coefficients from " + DEFAULT_RESOURCE, e); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| // Returns the environmental impacts for a given model ID. | ||
| public ModelImpacts getImpacts(String modelId) { | ||
| ModelImpacts impacts = modelsMap.get(modelId); | ||
| if (impacts == null && unknownModels.add(modelId)) { | ||
| LOG.warn("Unknown LLM model: {}", modelId); | ||
| } | ||
| return impacts; | ||
| } | ||
|
|
||
| /** | ||
| * Holds the environmental impact coefficients for a specific LLM model. | ||
| * The values are normalized to a block of 1,000 tokens, which is the standard | ||
| * billing unit for cloud inference services like AWS Bedrock. | ||
| */ | ||
| public static class ModelImpacts { | ||
| private final double energyKwhPer1kInputTokens; | ||
| private final double energyKwhPer1kOutputTokens; | ||
| private final double embodiedCo2eGPer1kTokens; | ||
|
|
||
| public ModelImpacts(double energyKwhPer1kInputTokens, double energyKwhPer1kOutputTokens, double embodiedCo2eGPer1kTokens) { | ||
| this.energyKwhPer1kInputTokens = energyKwhPer1kInputTokens; | ||
| this.energyKwhPer1kOutputTokens = energyKwhPer1kOutputTokens; | ||
| this.embodiedCo2eGPer1kTokens = embodiedCo2eGPer1kTokens; | ||
| } | ||
|
|
||
| public double getEnergyKwhPer1kInputTokens() { | ||
| return energyKwhPer1kInputTokens; | ||
| } | ||
|
|
||
| public double getEnergyKwhPer1kOutputTokens() { | ||
| return energyKwhPer1kOutputTokens; | ||
| } | ||
|
|
||
| public double getEmbodiedCo2eGPer1kTokens() { | ||
| return embodiedCo2eGPer1kTokens; | ||
| } | ||
| } | ||
| } |
7 changes: 7 additions & 0 deletions
7
src/main/java/com/digitalpebble/spruce/modules/ecologits/package-info.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| /** | ||
| * This package provides enrichment modules for estimating the environmental | ||
| * impact of LLM inference, using data derived from EcoLogits. | ||
| * | ||
| * @see <a href="https://ecologits.ai/">EcoLogits</a> | ||
| */ | ||
| package com.digitalpebble.spruce.modules.ecologits; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| { | ||
| "anthropic.claude-v2": { | ||
|
dpol1 marked this conversation as resolved.
|
||
| "aliases": ["Claude 2", "anthropic.claude-v2:1", "Claude 2.1"], | ||
| "energy_kwh_per_1k_input_tokens": 0.00017942, | ||
| "energy_kwh_per_1k_output_tokens": 0.00035885, | ||
| "embodied_co2e_g_per_1k_tokens": 0.00013821 | ||
| }, | ||
| "anthropic.claude-3-haiku-20240307-v1:0": { | ||
| "aliases": ["Claude 3 Haiku"], | ||
| "energy_kwh_per_1k_input_tokens": 0.00067550, | ||
| "energy_kwh_per_1k_output_tokens": 0.00135099, | ||
| "embodied_co2e_g_per_1k_tokens": 0.00052049 | ||
| }, | ||
| "anthropic.claude-3-sonnet-20240229-v1:0": { | ||
| "aliases": ["Claude 3 Sonnet"], | ||
| "energy_kwh_per_1k_input_tokens": 0.00081931, | ||
| "energy_kwh_per_1k_output_tokens": 0.00163861, | ||
| "embodied_co2e_g_per_1k_tokens": 0.00063081 | ||
| }, | ||
| "anthropic.claude-3-opus-20240229-v1:0": { | ||
| "aliases": ["Claude 3 Opus"], | ||
| "energy_kwh_per_1k_input_tokens": 0.00968710, | ||
| "energy_kwh_per_1k_output_tokens": 0.01937419, | ||
| "embodied_co2e_g_per_1k_tokens": 0.00744024 | ||
| }, | ||
| "amazon.titan-text-express-v1": { | ||
| "aliases": ["Titan Text G1 - Express"], | ||
| "energy_kwh_per_1k_input_tokens": 0.00002526, | ||
| "energy_kwh_per_1k_output_tokens": 0.00005051, | ||
| "embodied_co2e_g_per_1k_tokens": 0.00001952 | ||
| }, | ||
| "meta.llama3-8b-instruct-v1:0": { | ||
| "aliases": ["Llama 3 8B Instruct"], | ||
| "energy_kwh_per_1k_input_tokens": 0.00002551, | ||
| "energy_kwh_per_1k_output_tokens": 0.00005101, | ||
| "embodied_co2e_g_per_1k_tokens": 0.00001971 | ||
| }, | ||
| "meta.llama3-70b-instruct-v1:0": { | ||
| "aliases": ["Llama 3 70B Instruct"], | ||
| "energy_kwh_per_1k_input_tokens": 0.00018142, | ||
| "energy_kwh_per_1k_output_tokens": 0.00036283, | ||
| "embodied_co2e_g_per_1k_tokens": 0.00013974 | ||
| }, | ||
| "mistral.mistral-7b-instruct-v0:2": { | ||
| "aliases": ["Mistral 7B Instruct"], | ||
| "energy_kwh_per_1k_input_tokens": 0.00002526, | ||
| "energy_kwh_per_1k_output_tokens": 0.00005051, | ||
| "embodied_co2e_g_per_1k_tokens": 0.00001952 | ||
| }, | ||
| "mistral.mixtral-8x7b-instruct-v0:1": { | ||
| "aliases": ["Mixtral 8x7B Instruct"], | ||
| "energy_kwh_per_1k_input_tokens": 0.00005411, | ||
| "energy_kwh_per_1k_output_tokens": 0.00010821, | ||
| "embodied_co2e_g_per_1k_tokens": 0.00004179 | ||
| }, | ||
| "cohere.command-r-v1:0": { | ||
| "aliases": ["Command R"], | ||
| "energy_kwh_per_1k_input_tokens": 0.00006812, | ||
| "energy_kwh_per_1k_output_tokens": 0.00013625, | ||
| "embodied_co2e_g_per_1k_tokens": 0.00005255 | ||
| }, | ||
| "ai21.j2-ultra-v1": { | ||
| "aliases": ["Jurassic-2 Ultra"], | ||
| "energy_kwh_per_1k_input_tokens": 0.00018142, | ||
| "energy_kwh_per_1k_output_tokens": 0.00036283, | ||
| "embodied_co2e_g_per_1k_tokens": 0.00013974 | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.