Posted on
Artificial Intelligence

Artificial Intelligence Energy Monitoring

Author
  • User
    linuxbash
    Posts by this author
    Posts by this author

Artificial Intelligence Energy Monitoring on Linux with Bash: Measure, Optimize, Repeat

AI models are getting faster and larger—but they’re also getting hungrier. If you’ve ever watched your laptop fan take flight during a fine-tune or your server room bill creep up after deploying a new inference service, you already know: without energy visibility, AI is guesswork. The fix is straightforward and very Linux: measure what matters with a few CLI tools and shell scripts, then iterate.

This post shows you how to monitor and manage AI energy use on Linux with Bash. You’ll get:

  • Why AI energy monitoring is worth your time

  • A minimal, reproducible toolset you can install via apt, dnf, or zypper

  • Bash snippets to log CPU and (optionally) GPU power

  • Concrete steps to baseline, optimize, and enforce energy budgets

Why monitor AI energy?

  • Cost control: Power is a significant (and rising) fraction of TCO for GPU nodes and even CPU-heavy inference clusters.

  • Performance per watt: “Faster” models are not always more efficient. Knowing joules per sample beats eyeballing throughput.

  • Sustainability and compliance: Many orgs now track energy and carbon. Accurate, job-level metrics make reporting and tuning possible.

  • Capacity planning: Power caps, core pinning, and scheduling improve density—if you can quantify the effects.

What we’ll build

  • Quick baseline using powertop and turbostat

  • A small Bash wrapper to log CPU energy via RAPL and (optionally) GPU power via NVIDIA tools

  • Practical optimizations you can toggle and validate with hard numbers

  • A simple “energy budget guard” to fail CI if a job exceeds your target


Prerequisites and installation

The examples below use:

  • powertop (baseline and tuning hints)

  • turbostat (CPU package power and temps; Intel/AMD CPUs)

  • nvtop (optional, GPU overview; shows power if drivers expose it)

  • numactl (optional, CPU/NUMA placement)

  • gnuplot (optional, quick graphs)

Install with your package manager.

Apt (Debian/Ubuntu):

sudo apt update
sudo apt install powertop linux-tools-common nvtop numactl gnuplot
# Ensure turbostat matches your running kernel:
sudo apt install "linux-tools-$(uname -r)"

DNF (Fedora/RHEL derivatives):

sudo dnf install powertop kernel-tools nvtop numactl gnuplot

Zypper (openSUSE):

sudo zypper install powertop kernel-tools nvtop numactl gnuplot

Notes:

  • turbostat is part of linux-tools (apt) or kernel-tools (dnf/zypper).

  • GPU power telemetry requires proper vendor drivers. nvtop will display power where NVML/ROCm exposes it.

  • Reading RAPL energy counters may require root; use sudo for the scripts that touch /sys/class/powercap.


Step 1: Establish a baseline

Start with the simplest measurements to understand idle vs. load.

1) Calibrate and inspect with powertop (optional but useful on laptops/workstations):

sudo powertop --calibrate
sudo powertop
  • Use the Overview tab to see estimated power draw at idle and under light load.

  • Calibration can flicker devices; don’t run on production nodes.

2) Watch CPU package power with turbostat:

# 1-second updates, quiet summary, includes RAPL-based package power
sudo turbostat --Summary --quiet --interval 1
  • Keep this running in a side terminal while you kick off AI jobs to correlate spikes with workload phases.

3) Quick GPU sanity check (optional) with nvtop:

nvtop
  • If drivers provide telemetry, you’ll see GPU utilization, memory, and power draw.

Step 2: Log per-job energy with a Bash wrapper

For CPU energy, Linux exposes RAPL energy counters under /sys/class/powercap. We’ll sum top-level package domains and compute joules used during a command. If NVIDIA tools are present, we’ll also sample GPU power at 1 Hz in the background.

Create ai-energy-run.sh:

#!/usr/bin/env bash
set -euo pipefail

# Where to write CSV logs
LOG_FILE="${LOG_FILE:-ai_energy_log.csv}"

# Find top-level RAPL energy files (intel-rapl:0, amd-rapl:0, etc.)
rapl_top_level_files() {
  local p
  for p in /sys/class/powercap/*:*; do
    # Select only one-colon domains (e.g., intel-rapl:0) not subdomains (e.g., intel-rapl:0:0)
    [[ -d "$p" && "$p" != *:*:* ]] && [[ -f "$p/energy_uj" ]] && echo "$p/energy_uj"
  done
}

rapl_sum_uj() {
  local sum=0 val
  for f in $(rapl_top_level_files); do
    if [[ -r "$f" ]]; then
      val=$(<"$f")
      sum=$(( sum + val ))
    fi
  done
  echo "$sum"
}

need_root=false
for f in $(rapl_top_level_files); do
  [[ -r "$f" ]] || need_root=true
done
if $need_root; then
  echo "Note: RAPL energy counters require root on many systems. Re-run with sudo if you see permission errors." >&2
fi

cmd="${*:-}"
if [[ -z "$cmd" ]]; then
  echo "Usage: $0 <command to measure>" >&2
  exit 1
fi

# Optional GPU sampler if nvidia-smi is available and can read power
GPU_TMP=""
GPU_SAMPLER_PID=""
if command -v nvidia-smi >/dev/null 2>&1; then
  if nvidia-smi --query-gpu=power.draw --format=csv,noheader,nounits >/dev/null 2>&1; then
    GPU_TMP="$(mktemp)"
    # Sample total average power across all GPUs each second
    ( while :; do
        ts=$(date +%s)
        # Sum power across GPUs; ignore errors
        watts=$(nvidia-smi --query-gpu=power.draw --format=csv,noheader,nounits 2>/dev/null | awk '{s+=$1} END{print (s==0?0:s)}')
        [[ -n "$watts" ]] && echo "$ts,$watts" >> "$GPU_TMP"
        sleep 1
      done ) &
    GPU_SAMPLER_PID=$!
  fi
fi

start_ts=$(date +%s)
start_uj=$(rapl_sum_uj)
start_ns=$(date +%s%N)

# Run the target command
set +e
eval "$cmd"
exit_code=$?
set -e

end_ns=$(date +%s%N)
end_ts=$(date +%s)
end_uj=$(rapl_sum_uj)

# Cleanup GPU sampler
avg_gpu_watts=""
gpu_joules=""
if [[ -n "${GPU_SAMPLER_PID}" ]]; then
  kill "$GPU_SAMPLER_PID" >/dev/null 2>&1 || true
  wait "$GPU_SAMPLER_PID" 2>/dev/null || true
  if [[ -s "$GPU_TMP" ]]; then
    # Average watts during the run window
    avg_gpu_watts=$(awk -F, -v s="$start_ts" -v e="$end_ts" '($1>=s && $1<=e){n++; sum+=$2} END{if(n>0) printf("%.3f", sum/n)}' "$GPU_TMP")
  fi
  rm -f "$GPU_TMP"
fi

# Compute CPU package joules and wall clock
delta_uj=$(( end_uj - start_uj ))
# Handle wrap-around (rare, but possible)
if (( delta_uj < 0 )); then
  # Assume 64-bit wrap
  delta_uj=$(( (1<<63) + delta_uj ))
fi

wall_s=$(awk -v s="$start_ns" -v e="$end_ns" 'BEGIN{printf("%.6f",(e-s)/1e9)}')
cpu_joules=$(awk -v uj="$delta_uj" 'BEGIN{printf("%.6f", uj/1e6)}')

if [[ -n "$avg_gpu_watts" && -n "$wall_s" ]]; then
  gpu_joules=$(awk -v w="$avg_gpu_watts" -v t="$wall_s" 'BEGIN{printf("%.6f", w*t)}')
fi

# Write CSV header if needed
if [[ ! -f "$LOG_FILE" ]]; then
  echo "timestamp,command,exit_code,wall_s,cpu_joules,avg_gpu_watts,gpu_joules" > "$LOG_FILE"
fi

echo "$(date -Is),\"$cmd\",$exit_code,$wall_s,$cpu_joules,${avg_gpu_watts:-},${gpu_joules:-}" | tee -a "$LOG_FILE"
exit $exit_code

Make it executable:

chmod +x ai-energy-run.sh

Example usage:

# Measure a CPU inference script
sudo ./ai-energy-run.sh python inference.py --model small.onnx --batch 64

# Measure a GPU training step (NVIDIA systems)
sudo ./ai-energy-run.sh python train.py --epochs 1 --lr 3e-4

You’ll get a CSV log like:

timestamp,command,exit_code,wall_s,cpu_joules,avg_gpu_watts,gpu_joules
2026-07-11T10:25:09+00:00,"python train.py --epochs 1",0,42.318742,1450.221,128.3,5425.884

Interpretation:

  • cpu_joules: Energy used by CPU package(s) during the run (J).

  • avg_gpu_watts: Mean GPU power across all GPUs (W).

  • gpu_joules: Approx energy = avg watts × time (J). This is an approximation from 1 Hz sampling.


Step 3: Optimize and verify

Small tweaks can move the needle. Use the wrapper to validate before/after.

1) Right-size CPU threads for math libraries:

# Start with physical cores, not threads
export OMP_NUM_THREADS=$(nproc --ignore=1)
export MKL_NUM_THREADS=$OMP_NUM_THREADS
export OPENBLAS_NUM_THREADS=$OMP_NUM_THREADS
sudo ./ai-energy-run.sh python cpu_job.py
  • Try different values (e.g., half cores) and compare joules/sample.

2) Pin work to specific CPUs/NUMA nodes:

# Pin to NUMA node 0 and first 8 cores
sudo ./ai-energy-run.sh numactl --cpunodebind=0 --physcpubind=0-7 python cpu_job.py
  • Often reduces cross-node traffic and energy.

3) NVIDIA power caps and persistence mode (if supported):

# Enable persistence mode
sudo nvidia-smi -pm 1
# Set a power limit (adjust to your GPU’s allowable range)
sudo nvidia-smi -pl 180
# Re-run your job and compare energy vs. throughput
sudo ./ai-energy-run.sh python train.py --epochs 1
  • Many workloads achieve near-identical throughput at a lower cap, cutting joules.

4) System-wide power tuning (mainly laptops/workstations):

# Apply suggested tunings (verify changes fit your use case)
sudo powertop --auto-tune
  • Reboot resets changes; add to boot scripts if desired.

5) Batch size and precision:

  • Try smaller batch sizes on latency-sensitive inference to reduce peak power.

  • Use lower precision (bf16/fp16/int8) where accuracy permits; measure before/after.


Step 4: Add an “energy budget” guard

Block regressions in CI by failing when a job exceeds a joule target.

Create guard-energy.sh:

#!/usr/bin/env bash
set -euo pipefail

LOG_FILE="${1:-ai_energy_log.csv}"
MAX_JOULES="${2:-2000}"   # default 2 kJ
LAST=$(tail -n 1 "$LOG_FILE")
cpu_joules=$(echo "$LAST" | awk -F, 'NR==1{print $(NF-2)}')  # cpu_joules field
# Fallback if quoting confuses commas
if [[ -z "$cpu_joules" ]]; then
  cpu_joules=$(echo "$LAST" | awk -F, '{print $(NF-2)}')
fi

awk -v j="$cpu_joules" -v m="$MAX_JOULES" 'BEGIN{
  if (j+0 > m+0) { printf("FAIL: %.3f J > %.3f J\n", j, m); exit 1 }
  else { printf("PASS: %.3f J <= %.3f J\n", j, m); exit 0 }
}'

Use it after a run:

./guard-energy.sh ai_energy_log.csv 1500

Real-world example playbook

Try this sequence to see tangible gains: 1) Baseline:

sudo ./ai-energy-run.sh python train.py --epochs 1 --model tiny

2) Cap GPU and set threads:

export OMP_NUM_THREADS=$(nproc)
sudo nvidia-smi -pm 1
sudo nvidia-smi -pl 170
sudo ./ai-energy-run.sh python train.py --epochs 1 --model tiny

3) Pin CPUs and reduce batch size slightly:

sudo ./ai-energy-run.sh numactl --physcpubind=0-15 python train.py --epochs 1 --model tiny --batch 80

4) Compare cpu_joules and gpu_joules across runs; pick the best energy/throughput trade-off.

Optional: plot wall_s vs. total joules with gnuplot:

gnuplot -persist <<'PLOT'
set datafile separator comma
set key autotitle columnhead
set xlabel "Run"
set ylabel "Joules"
plot "<tail -n +2 ai_energy_log.csv | nl -v 1 -s, -w1" using 1:5 with linespoints title "CPU J", \
     "" using 1:7 with linespoints title "GPU J"
PLOT

Troubleshooting

  • Permission denied on energy_uj: run with sudo or adjust udev permissions for /sys/class/powercap.

  • No GPU power shown: ensure vendor drivers are installed and telemetry is exposed. nvtop and nvidia-smi rely on NVML; AMD requires ROCm/AMDGPU telemetry.

  • turbostat not found: install linux-tools (apt) or kernel-tools (dnf/zypper) and ensure versions match your kernel.


Conclusion and next steps

You don’t need heavyweight tooling to get actionable AI energy metrics. With turbostat, powertop, and a ~100-line Bash wrapper, you can:

  • Measure job-level joules on CPU and approximate GPU energy

  • Iterate on threads, placement, batch size, and power caps

  • Enforce energy budgets in CI

Your next step:

  • Install the tools, wrap your most common training/inference command with ai-energy-run.sh, and record three runs with different settings. Keep the configuration that gives the best performance per joule.

  • Then, add guard-energy.sh to your CI to prevent regressions.

Have a trick that saves watts without hurting throughput? Share it—your future self (and your power bill) will thank you.