Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ go.work.sum
tmp

coverage.*
*.patch
/metrics
22 changes: 21 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ GOOS ?= $(shell go env GOOS)
GOARCH ?= $(shell go env GOARCH)

CGO_ENABLED ?= 1
CC ?= cc
CXX ?= c++

# Cross-compilation sysroot auto-detection
# On Fedora, the cross-compiler's default sysroot lacks headers.
Expand Down Expand Up @@ -74,6 +76,24 @@ endif

LDFLAGS=-ldflags "$(LD_STRIP_DEBUG_SYMBOLS) $(LD_VERSION_FLAGS)"

# Build tags (e.g., TAGS=goamdsmi)
TAGS ?=
ifneq ($(TAGS),)
BUILD_ARGS=-tags $(TAGS)
endif

# When goamdsmi tag is used, ensure C++ stdlib is linked
# The goamdsmi library requires C++ linking
ifneq (,$(findstring goamdsmi,$(TAGS)))
BUILD_ENV_VARS=CC="$(CC)" CXX="$(CXX)" CGO_ENABLED=1
# Preserve user CGO_CFLAGS and CGO_LDFLAGS if set, add C++ stdlib
export CGO_CFLAGS
export CGO_LDFLAGS += -lstdc++
export CGO_CXXFLAGS=-std=c++11
else
BUILD_ENV_VARS=CC="$(CC)"
endif

BUILD_DEBUG_ARGS ?=

# Docker parameters
Expand Down Expand Up @@ -130,7 +150,7 @@ help: ## Show this help message
.PHONY: build
build: ## Build binary
mkdir -p $(BINARY_DIR)
GOOS=$(GOOS) GOARCH=$(GOARCH) CGO_ENABLED=$(CGO_ENABLED) CC=$(CC) \
GOOS=$(GOOS) GOARCH=$(GOARCH) CGO_ENABLED=$(CGO_ENABLED) $(BUILD_ENV_VARS) \
$(GOBUILD) $(BUILD_ARGS) \
$(LDFLAGS) \
-o $(BINARY_DIR)/$(BINARY_NAME) \
Expand Down
4 changes: 2 additions & 2 deletions cmd/kepler/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"github.com/alecthomas/kingpin/v2"

"github.com/sustainable-computing-io/kepler/config"
"github.com/sustainable-computing-io/kepler/internal/device"
"github.com/sustainable-computing-io/kepler/internal/device/cpu"
"github.com/sustainable-computing-io/kepler/internal/device/gpu"
_ "github.com/sustainable-computing-io/kepler/internal/device/gpu/nvidia" // Register NVIDIA backend
"github.com/sustainable-computing-io/kepler/internal/exporter/prometheus"
Expand Down Expand Up @@ -130,7 +130,7 @@ Configuration

func createServices(logger *slog.Logger, cfg *config.Config) ([]service.Service, error) {
logger.Debug("Creating all services")
cpuPowerMeter, err := device.CreateCPUMeter(logger, cfg)
cpuPowerMeter, err := cpu.CreateCPUMeter(logger, cfg)
if err != nil {
return nil, fmt.Errorf("failed to create CPU power meter: %w", err)
}
Expand Down
2 changes: 2 additions & 0 deletions compose/default/kepler/etc/kepler/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ experimental:
forceEnabled: false # DEPRECATED: set cpu.preferredMeters: ["hwmon"] instead. Emits a deprecation warning when true.
zones: [] # List of zones to enable (default enable all)
chipRules: [] # User-defined chip pairing rules (override/add to hardcoded defaults)
esmi:
enabled: false # Enable experimental AMD ESMI power monitoring (prepends "esmi" to cpu.preferredMeters)
gpu:
enabled: false # Enable experimental GPU power monitoring
idlePower: 0 # GPU idle power in Watts (0 = auto-detect)
Expand Down
2 changes: 2 additions & 0 deletions compose/dev/kepler-dev/etc/kepler/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ experimental:
forceEnabled: false # DEPRECATED: set cpu.preferredMeters: ["hwmon"] instead. Emits a deprecation warning when true.
zones: [] # List of zones to enable (default enable all)
chipRules: [] # User-defined chip pairing rules (override/add to hardcoded defaults)
esmi:
enabled: false # Enable experimental AMD ESMI power monitoring (prepends "esmi" to cpu.preferredMeters)
gpu:
enabled: false # Enable experimental GPU power monitoring
idlePower: 0 # GPU idle power in Watts (0 = auto-detect)
Expand Down
60 changes: 58 additions & 2 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ const (

// ExperimentalGPUFeature represents GPU power monitoring (experimental)
ExperimentalGPUFeature Feature = "gpu"

// ExperimentalEsmiFeature represents the CPU ESMI power monitoring feature
ExperimentalEsmiFeature Feature = "esmi"
)

// Config represents the complete application configuration
Expand Down Expand Up @@ -189,11 +192,17 @@ type (
DCGMEndpoint string `yaml:"dcgmEndpoint"`
}

// ESMI configuration (Experimental)
Esmi struct {
Enabled *bool `yaml:"enabled"` //Development mode (capability auto detection in future)
}

// Experimental contains experimental features (no stability guarantees)
Experimental struct {
Platform Platform `yaml:"platform"`
Hwmon Hwmon `yaml:"hwmon"`
GPU ExperimentalGPU `yaml:"gpu"`
Esmi Esmi `yaml:"esmi"`
}

Config struct {
Expand Down Expand Up @@ -311,6 +320,9 @@ const (
ExperimentalHwmonForceEnabledFlag = "experimental.hwmon.force-enabled"
ExperimentalHwmonZonesFlag = "experimental.hwmon.zones"

// Experimental ESMI flags
ExperimentalEsmiEnabledFlag = "experimental.esmi.enabled"

// Experimental GPU flags
ExperimentalGPUEnabledFlag = "experimental.gpu.enabled"
ExperimentalGPUIdlePowerFlag = "experimental.gpu.idle-power"
Expand Down Expand Up @@ -390,6 +402,10 @@ func DefaultConfig() *Config {
// operators who set them today expect the legacy behavior. When both legacy
// keys are set, fake takes precedence over hwmon. The legacy keys will stop
// working in a future release.
//
// When experimental.esmi.enabled is set, "esmi" is prepended to the
// cpu.preferredMeters list (unless already present), making ESMI the
// first-tried CPU meter.
func (c *Config) ApplyCpuMeterDeprecations(logger *slog.Logger) {
switch {
case ptr.Deref(c.Dev.FakeCpuMeter.Enabled, false):
Expand All @@ -399,6 +415,23 @@ func (c *Config) ApplyCpuMeterDeprecations(logger *slog.Logger) {
logger.Warn(`experimental.hwmon.forceEnabled is deprecated; set cpu.preferredMeters: ["hwmon"] instead`)
c.Cpu.PreferredMeters = []string{"hwmon"}
}

// When ESMI is explicitly enabled, inject it as the first CPU meter to try
if c.Experimental != nil && ptr.Deref(c.Experimental.Esmi.Enabled, false) {
// Check if "esmi" is already in the list
hasEsmi := false
for _, m := range c.Cpu.PreferredMeters {
if m == "esmi" {
hasEsmi = true
break
}
}
if !hasEsmi {
// Prepend "esmi" to try it first
c.Cpu.PreferredMeters = append([]string{"esmi"}, c.Cpu.PreferredMeters...)
logger.Info("esmi CPU meter enabled", "preferredMeters", c.Cpu.PreferredMeters)
}
}
}

// Load loads configuration from an io.Reader
Expand Down Expand Up @@ -501,6 +534,9 @@ func RegisterFlags(app *kingpin.Application) ConfigUpdaterFn {
hwmonForceEnabled := app.Flag(ExperimentalHwmonForceEnabledFlag, "Force hwmon as the power meter, skipping RAPL auto-detection").Default("false").Bool()
hwmonZones := app.Flag(ExperimentalHwmonZonesFlag, "Hwmon zone filter (power labels to monitor)").Strings()

// experimental ESMI
esmiEnabled := app.Flag(ExperimentalEsmiEnabledFlag, "Enable experimental ESMI power monitoring").Default("false").Bool()

// experimental GPU
gpuEnabled := app.Flag(ExperimentalGPUEnabledFlag, "Enable experimental GPU power monitoring").Default("false").Bool()
gpuIdlePower := app.Flag(ExperimentalGPUIdlePowerFlag, "GPU idle power in Watts (0 = auto-detect from idle observations)").Default("0").Float64()
Expand Down Expand Up @@ -578,6 +614,15 @@ func RegisterFlags(app *kingpin.Application) ConfigUpdaterFn {
return err
}

// Apply experimental ESMI settings
if flagsSet[ExperimentalEsmiEnabledFlag] {
// Initialize experimental section if needed
if cfg.Experimental == nil {
cfg.Experimental = &Experimental{}
}
cfg.Experimental.Esmi.Enabled = esmiEnabled
}

// Apply experimental GPU settings
applyGPUConfig(cfg, flagsSet, gpuEnabled, gpuIdlePower, gpuDCGMEndpoint)

Expand Down Expand Up @@ -785,6 +830,11 @@ func (c *Config) IsFeatureEnabled(feature Feature) bool {
return ptr.Deref(c.Exporter.Stdout.Enabled, false)
case PprofFeature:
return ptr.Deref(c.Debug.Pprof.Enabled, false)
case ExperimentalEsmiFeature:
if c.Experimental == nil {
return false
}
return ptr.Deref(c.Experimental.Esmi.Enabled, false)
case ExperimentalGPUFeature:
if c.Experimental == nil {
return false
Expand All @@ -811,6 +861,11 @@ func (c *Config) experimentalFeatureEnabled() bool {
return true
}

// Check if ESMI is enabled
if ptr.Deref(c.Experimental.Esmi.Enabled, false) {
return true
}

// Check if GPU is enabled
if ptr.Deref(c.Experimental.GPU.Enabled, false) {
return true
Expand Down Expand Up @@ -924,15 +979,16 @@ func (c *Config) Validate(skips ...SkipValidation) error {
}
}
{ // cpu.preferredMeters
// Keep this list in sync with the switch in internal/device/cpu_power_meter.go.
// Keep this list in sync with the switch in internal/device/cpu/cpu_power_meter.go.
validCpuMeters := map[string]bool{
"rapl": true,
"hwmon": true,
"esmi": true,
"fake": true,
}
for _, name := range c.Cpu.PreferredMeters {
if !validCpuMeters[name] {
errs = append(errs, fmt.Sprintf("invalid cpu.preferredMeters entry %q, must be one of %q, %q, %q", name, "rapl", "hwmon", "fake"))
errs = append(errs, fmt.Sprintf("invalid cpu.preferredMeters entry %q, must be one of %q, %q, %q, %q", name, "rapl", "hwmon", "esmi", "fake"))
}
}
}
Expand Down
19 changes: 18 additions & 1 deletion config/config_cpu_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,23 @@ func TestCpuPreferredMeters(t *testing.T) {
},
want: []string{"rapl"},
},
{
name: "esmi enabled prepends to preferredMeters",
setup: func(c *Config) {
c.Experimental = &Experimental{}
c.Experimental.Esmi.Enabled = ptr.To(true)
},
want: []string{"esmi", "rapl", "hwmon"},
},
{
name: "esmi already in list: no duplication",
setup: func(c *Config) {
c.Cpu.PreferredMeters = []string{"esmi", "rapl"}
c.Experimental = &Experimental{}
c.Experimental.Esmi.Enabled = ptr.To(true)
},
want: []string{"esmi", "rapl"},
},
}

logger := slog.New(slog.DiscardHandler)
Expand All @@ -81,7 +98,7 @@ func TestCpuPreferredMetersValidation(t *testing.T) {
}{
{
name: "all known backends",
meters: []string{"rapl", "hwmon", "fake"},
meters: []string{"rapl", "hwmon", "esmi", "fake"},
},
{
name: "unknown backend",
Expand Down
24 changes: 24 additions & 0 deletions docs/user/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ You can configure Kepler by passing flags when starting the service. The followi
| `--experimental.platform.redfish.config-file` | Path to experimental Redfish BMC configuration file | `""` | Any valid file path |
| `--experimental.hwmon.force-enabled` | Force hwmon as power meter, skipping RAPL auto-detection | `false` | `true`, `false` |
| `--experimental.hwmon.zones` | hwmon zones to be enabled (can be specified multiple times) | All available zones | Any valid hwmon zone name |
| `--experimental.esmi.enabled` | Enable experimental AMD ESMI power monitoring for AMD EPYC CPUs | `false` | `true`, `false` |
| `--experimental.gpu.enabled` | Enable experimental GPU power monitoring | `false` | `true`, `false` |
| `--experimental.gpu.idle-power` | GPU idle power in Watts (0 = auto-detect) | `0` | Any non-negative float |
| `--experimental.gpu.dcgm-endpoint` | dcgm-exporter metrics endpoint for MIG power attribution | `""` (auto-discover) | URL (e.g., `http://10.0.0.1:9400/metrics`) |
Expand Down Expand Up @@ -249,6 +250,7 @@ Built-in backends:

- `rapl`: Intel RAPL via sysfs (default first choice)
- `hwmon`: hwmon power sensors (default second choice)
- `esmi`: AMD ESMI (E-SMI) for AMD EPYC processors (experimental, opt-in)
- `fake`: synthetic readings for development and testing

Examples:
Expand Down Expand Up @@ -444,6 +446,8 @@ experimental:
forceEnabled: false
zones: []
chipRules: []
esmi:
enabled: false
gpu:
enabled: false
```
Expand Down Expand Up @@ -546,6 +550,26 @@ experimental:
skipVoltages: [0] # Skip shunt voltage at in0
```

#### ESMI CPU Power Monitoring (AMD)

- **enabled**: Enable experimental AMD ESMI power monitoring (default: false)
- When enabled, Kepler will prepend `esmi` to `cpu.preferredMeters`, making it the first-tried CPU power source
- Collects power metrics from AMD EPYC processors via E-SMI library or sysfs
- Provides per-socket energy (package-N), per-thread energy (core-N), and per-socket DIMM power (dimm-N)
- Supports two backends:
- **goamdsmi**: Uses E-SMI C library (`libe_smi64.so`) - requires build with `-tags goamdsmi`
- **sysfs**: Pure Go fallback using `amd_energy` kernel module (default, no dependencies)

**Example:**

```yaml
experimental:
esmi:
enabled: true
```

When enabled, this configuration automatically prepends `esmi` to the CPU power meter priority list, so on AMD EPYC systems with ESMI support, it becomes the first choice before RAPL or hwmon.

#### GPU Power Monitoring

- **enabled**: Enable experimental GPU power monitoring (default: false)
Expand Down
3 changes: 3 additions & 0 deletions hack/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ cpu:
# Ordered preference list of CPU power-meter backends. The first backend
# that initializes successfully and reports zones is used.
# Built-in backends: rapl, hwmon, fake.
# To enable ESMI (AMD CPU power monitoring), set experimental.esmi.enabled: true
preferredMeters: [rapl, hwmon]

rapl:
Expand Down Expand Up @@ -111,3 +112,5 @@ experimental:
enabled: false # Enable experimental GPU power monitoring
idlePower: 0 # GPU idle power in Watts (0 = auto-detect)
dcgmEndpoint: "" # dcgm-exporter metrics URL for MIG (auto-discovered if empty)
esmi:
enabled: false # Enable experimental AMD ESMI CPU power monitoring (AMD EPYC platforms)
22 changes: 22 additions & 0 deletions internal/device/cpu/cpu_power_meter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: 2025 The Kepler Authors
// SPDX-License-Identifier: Apache-2.0

package cpu

import (
"github.com/sustainable-computing-io/kepler/internal/device"
)

// CPUPowerMeter is the interface for CPU power measurement.
// It embeds PowerMeter and adds CPU-specific methods.
type CPUPowerMeter interface {
device.PowerMeter

// Zones() returns a slice of the energy measurement zones
Zones() ([]device.EnergyZone, error)

// PrimaryEnergyZone() returns the zone with the highest energy coverage/priority
// This zone represents the most comprehensive energy measurement available
// E.g. Psys > Package > Core > DRAM > Uncore
PrimaryEnergyZone() (device.EnergyZone, error)
}
Loading
Loading