AID-D-005.005: Accelerator Telemetry Anomaly Detection

Version 1.20261015 · Actionable sub-technique

Continuously baseline and monitor accelerator telemetry (power, temperature, utilization, PMCs). Alert on deviations indicating cryptomining, DoS, or side-channel probing.

Pillars: infra

Lifecycle: operation

Scope boundary

Owns anomaly findings from AI accelerator power, temperature, utilization, and performance-counter telemetry against representative workload baselines, including cryptomining, denial-of-service, and side-channel-probing signals.

AID-D-002: AID-D-005.005 detects anomalies in GPU, TPU, NPU, or accelerator hardware telemetry; AID-D-002 detects drift and anomalies in model inputs, outputs, labeled performance, or model-internal signals. Hardware resource behavior and model behavioral drift may correlate but have different baselines and observation points.

Implementation guidance

AID-D-005.005-G001

Establish statistical baselines (mean/std) under representative workloads; compare live metrics and alert on 3-sigma deviations.
Concept:

To detect what is abnormal, first define workload-specific normal. A single global baseline is too noisy for mixed inference, batch, and training clusters. Build a baseline per accelerator class and workload profile, persist mean/stddev values, then compare sustained live telemetry against that profile instead of against ad hoc dashboard intuition.

Step 1: Build a workload baseline from Prometheus/DCGM data

Collect accelerator telemetry through NVIDIA DCGM Exporter or equivalent and store it in Prometheus. Train baselines only from known-good windows that represent the workload you actually want to protect.

# File: accelerator_telemetry/baseline_policy.schema.yaml
schema_version: aidefend.accelerator-baseline-policy-schema.v1
signed_policy_required_fields:
  - schema_version
  - policy_version
  - prometheus_url
  - baseline_window_seconds
  - query_step_seconds
  - minimum_samples
  - upper_sigma
  - minimum_scale
  - alert_after_consecutive_breaches
  - request_timeout_seconds
  - maximum_sample_age_seconds
  - maximum_future_skew_seconds
  - response_max_bytes
  - response_chunk_bytes
  - metrics
numeric_rule: finite policy values with the documented positive or non-negative domain
evidence_bindings:
  - policy_version
  - policy_sha256
  - workload_label
  - metric_population
  - source_window
  - sample_counts
from __future__ import annotations

# File: accelerator_telemetry/build_baseline.py
import hashlib
import json
import math
import os
import subprocess
from datetime import datetime, timedelta, timezone
from pathlib import Path
import hmac
import re
import tempfile
from urllib.parse import urlparse




def _aidefend_write_private(path: Path, payload: bytes) -> None:
    descriptor = os.open(
        path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600,
    )
    with os.fdopen(descriptor, "wb") as stream:
        stream.write(payload)
        stream.flush()
        os.fsync(stream.fileno())
    os.chmod(path, 0o400)


def _aidefend_strict_json(raw: bytes) -> object:
    def unique_object(pairs):
        value = {}
        for key, item in pairs:
            if key in value:
                raise ValueError("duplicate JSON key: " + key)
            value[key] = item
        return value

    return json.loads(
        raw.decode("utf-8", errors="strict"),
        object_pairs_hook=unique_object,
        parse_constant=lambda value: (_ for _ in ()).throw(
            ValueError("non-finite JSON constant: " + value)
        ),
    )


def _aidefend_capture_signed_bytes(
    payload_path: Path,
    proof_path: Path,
    key_reference: str,
    timeout_seconds: float,
    proof_flag: str,
) -> bytes:
    if payload_path == proof_path or proof_flag != "--bundle":
        raise RuntimeError("signed payload/proof inputs are invalid")
    payload_raw = payload_path.read_bytes()
    proof_raw = proof_path.read_bytes()
    remote_key = bool(re.match(
        r"^[A-Za-z][A-Za-z0-9+.-]*://", key_reference
    )) or key_reference.startswith("pkcs11:")
    key_raw = None if remote_key else (
        key_reference.encode("utf-8")
        if key_reference.startswith("-----BEGIN ")
        else Path(key_reference).read_bytes()
    )
    with tempfile.TemporaryDirectory(prefix="aidefend-signed-input-") as directory:
        root = Path(directory)
        os.chmod(root, 0o700)
        payload_snapshot = root / "payload"
        proof_snapshot = root / "proof"
        _aidefend_write_private(payload_snapshot, payload_raw)
        _aidefend_write_private(proof_snapshot, proof_raw)
        captured = [(payload_snapshot, payload_raw), (proof_snapshot, proof_raw)]
        key_argument = key_reference
        if key_raw is not None:
            key_snapshot = root / "verify-key.pub"
            _aidefend_write_private(key_snapshot, key_raw)
            captured.append((key_snapshot, key_raw))
            key_argument = str(key_snapshot)
        try:
            subprocess.run(
                [
                    "cosign", "verify-blob", "--key", key_argument,
                    "--bundle", str(proof_snapshot), str(payload_snapshot),
                ],
                check=True, capture_output=True, text=True, timeout=timeout_seconds,
            )
            if any(
                not hmac.compare_digest(path.read_bytes(), expected)
                for path, expected in captured
            ):
                raise RuntimeError("private signature-verification snapshot changed")
        finally:
            for path, _ in captured:
                os.chmod(path, 0o600)
    return payload_raw


import requests


def load_signed_accelerator_policy() -> tuple[dict, str]:
    policy_path = Path(os.environ["D005005_ACCELERATOR_POLICY_PATH"])
    bundle_path = Path(os.environ["D005005_ACCELERATOR_POLICY_SIGSTORE_BUNDLE"])
    verify_timeout = float(os.environ["AIDEFEND_POLICY_VERIFY_TIMEOUT_SECONDS"])
    if not math.isfinite(verify_timeout) or verify_timeout <= 0:
        raise RuntimeError("signed bootstrap-profile verification timeout is invalid")
    raw = _aidefend_capture_signed_bytes(
        policy_path, bundle_path, os.environ["D005005_ACCELERATOR_POLICY_VERIFY_KEY"],
        verify_timeout, "--bundle",
    )
    policy = _aidefend_strict_json(raw)
    required = {
        "schema_version", "policy_version", "prometheus_url",
        "baseline_window_seconds", "query_step_seconds", "minimum_samples",
        "upper_sigma", "minimum_scale", "alert_after_consecutive_breaches",
        "request_timeout_seconds", "maximum_sample_age_seconds",
        "maximum_future_skew_seconds", "response_max_bytes",
        "response_chunk_bytes", "metrics",
    }
    if not isinstance(policy, dict) or set(policy) != required:
        raise RuntimeError("signed accelerator policy fields differ")
    if (
        policy["schema_version"] != "aidefend.accelerator-baseline-policy.v1"
        or not isinstance(policy["policy_version"], str)
        or not policy["policy_version"]
        or not isinstance(policy["prometheus_url"], str)
        or urlparse(policy["prometheus_url"]).scheme != "https"
    ):
        raise RuntimeError("signed accelerator policy identity or endpoint is invalid")
    for field in (
        "baseline_window_seconds", "query_step_seconds", "minimum_samples",
        "alert_after_consecutive_breaches", "response_max_bytes",
        "response_chunk_bytes",
    ):
        value = policy[field]
        if isinstance(value, bool) or not isinstance(value, int) or value < 1:
            raise RuntimeError("signed accelerator integer is invalid: " + field)
    for field in (
        "upper_sigma", "minimum_scale", "request_timeout_seconds",
        "maximum_sample_age_seconds",
    ):
        value = policy[field]
        if (
            isinstance(value, bool)
            or not isinstance(value, (int, float))
            or not math.isfinite(float(value))
            or float(value) <= 0
        ):
            raise RuntimeError("signed accelerator bound is invalid: " + field)
    if (policy["response_max_bytes"] < 2
            or policy["response_chunk_bytes"] > policy["response_max_bytes"]):
        raise RuntimeError("signed Prometheus response bounds are invalid")
    future_skew = policy["maximum_future_skew_seconds"]
    if (
        isinstance(future_skew, bool)
        or not isinstance(future_skew, (int, float))
        or not math.isfinite(float(future_skew))
        or float(future_skew) < 0
    ):
        raise RuntimeError("signed accelerator future-skew bound is invalid")
    metrics = policy["metrics"]
    if (
        not isinstance(metrics, list)
        or not metrics
        or any(not isinstance(metric, str) or not metric for metric in metrics)
        or len(metrics) != len(set(metrics))
    ):
        raise RuntimeError("signed accelerator metric population is invalid")
    return policy, "sha256:" + hashlib.sha256(raw).hexdigest()


POLICY, POLICY_SHA256 = load_signed_accelerator_policy()


def reject_duplicate_keys(pairs):
    value = {}
    for key, item in pairs:
        if key in value:
            raise ValueError(f"duplicate Prometheus JSON key: {key}")
        value[key] = item
    return value


def read_prometheus_json(response):
    try:
        if response.headers.get("Content-Encoding", "identity").lower() != "identity":
            raise RuntimeError("encoded Prometheus response is forbidden")
        declared = response.headers.get("Content-Length")
        maximum = policy_maximum = POLICY["response_max_bytes"]
        if declared is not None and (not declared.isdigit() or int(declared) > maximum):
            raise RuntimeError("Prometheus Content-Length is invalid")
        chunks, observed = [], 0
        for chunk in response.iter_content(chunk_size=POLICY["response_chunk_bytes"]):
            observed += len(chunk)
            if observed > policy_maximum:
                raise RuntimeError("Prometheus response exceeds signed bound")
            chunks.append(chunk)
        if declared is not None and int(declared) != observed:
            raise RuntimeError("Prometheus Content-Length differs from observed bytes")
        return json.loads(b"".join(chunks).decode("utf-8", errors="strict"), object_pairs_hook=reject_duplicate_keys, parse_constant=lambda value: (_ for _ in ()).throw(ValueError('non-finite JSON constant: ' + value)))
    finally:
        response.close()


def query_range(query: str, start: datetime, end: datetime) -> list[float]:
    response = requests.get(
        POLICY["prometheus_url"].rstrip("/") + "/api/v1/query_range",
        params={
            "query": query,
            "start": start.isoformat(),
            "end": end.isoformat(),
            "step": str(POLICY["query_step_seconds"]) + "s",
        },
        timeout=float(POLICY["request_timeout_seconds"]),
        allow_redirects=False,
        stream=True,
        headers={"Accept-Encoding": "identity"},
    )
    response.raise_for_status()
    payload = read_prometheus_json(response)
    if payload.get("status") != "success":
        raise RuntimeError("Prometheus range query did not succeed")
    series = payload.get("data", {}).get("result")
    if not isinstance(series, list):
        raise RuntimeError("Prometheus range result schema differs")
    values: list[float] = []
    for item in series:
        points = item.get("values") if isinstance(item, dict) else None
        if not isinstance(points, list):
            raise RuntimeError("Prometheus range series schema differs")
        for point in points:
            if not isinstance(point, list) or len(point) != 2:
                raise RuntimeError("Prometheus sample schema differs")
            value = float(point[1])
            if not math.isfinite(value):
                raise RuntimeError("Prometheus sample is non-finite")
            values.append(value)
    return values


def build_baseline(workload_label: str, output_path: str) -> dict:
    if not isinstance(workload_label, str) or not workload_label:
        raise ValueError("trusted workload label is required")
    end = datetime.now(timezone.utc)
    start = end - timedelta(seconds=POLICY["baseline_window_seconds"])
    baseline = {
        "schema_version": "aidefend.accelerator-baseline.v1",
        "policy_version": POLICY["policy_version"],
        "policy_sha256": POLICY_SHA256,
        "workload_label": workload_label,
        "window_start": start.isoformat(),
        "window_end": end.isoformat(),
        "metrics": {},
    }
    for metric in POLICY["metrics"]:
        values = query_range(
            metric + '{workload="' + workload_label + '"}',
            start,
            end,
        )
        if len(values) < POLICY["minimum_samples"]:
            raise ValueError("metric sample population is below signed policy: " + metric)
        mean = math.fsum(values) / len(values)
        variance = math.fsum((value - mean) ** 2 for value in values) / len(values)
        baseline["metrics"][metric] = {
            "mean": mean,
            "stdev": math.sqrt(variance),
            "sample_count": len(values),
        }
    target = Path(output_path)
    target.write_text(
        json.dumps(baseline, sort_keys=True, separators=(",", ":")) + "\n",
        encoding="utf-8",
    )
    if json.loads(target.read_text(encoding="utf-8")) != baseline:
        raise RuntimeError("accelerator baseline readback differs")
    return baseline
Step 2: Alert on sustained policy-threshold deviations

Evaluate live values against the saved baseline and require multiple consecutive breaches before alerting. This reduces false positives from bursty but normal accelerator usage.

# Continuing in accelerator_telemetry/build_baseline.py
def latest_value(query: str) -> tuple[float, float]:
    response = requests.get(
        POLICY["prometheus_url"].rstrip("/") + "/api/v1/query",
        params={"query": query},
        timeout=float(POLICY["request_timeout_seconds"]),
        allow_redirects=False,
        stream=True,
        headers={"Accept-Encoding": "identity"},
    )
    response.raise_for_status()
    payload = read_prometheus_json(response)
    result = payload.get("data", {}).get("result")
    if payload.get("status") != "success" or not isinstance(result, list) or len(result) != 1:
        raise ValueError("instant query must return exactly one series")
    point = result[0].get("value")
    if not isinstance(point, list) or len(point) != 2:
        raise ValueError("instant-query sample schema differs")
    observed_at = float(point[0])
    value = float(point[1])
    age = datetime.now(timezone.utc).timestamp() - observed_at
    if (
        not math.isfinite(value)
        or not math.isfinite(age)
        or age > float(POLICY["maximum_sample_age_seconds"])
        or age < -float(POLICY["maximum_future_skew_seconds"])
    ):
        raise ValueError("instant-query sample is non-finite or outside signed freshness")
    return value, observed_at


def detect_gpu_anomalies(
    workload_label: str,
    baseline: dict,
    consecutive_breaches: dict[str, int],
) -> dict:
    if (
        baseline.get("policy_version") != POLICY["policy_version"]
        or baseline.get("policy_sha256") != POLICY_SHA256
        or baseline.get("workload_label") != workload_label
        or set(baseline.get("metrics", {})) != set(POLICY["metrics"])
    ):
        raise RuntimeError("accelerator baseline binding or metric population differs")
    findings = []
    observations = []
    for metric in POLICY["metrics"]:
        live, observed_at = latest_value(
            metric + '{workload="' + workload_label + '"}'
        )
        stats = baseline["metrics"][metric]
        scale = max(float(stats["stdev"]), float(POLICY["minimum_scale"]))
        threshold = float(stats["mean"]) + float(POLICY["upper_sigma"]) * scale
        if live > threshold:
            consecutive_breaches[metric] = consecutive_breaches.get(metric, 0) + 1
        else:
            consecutive_breaches[metric] = 0
        observation = {
            "metric": metric,
            "live": live,
            "observed_at_unix": observed_at,
            "threshold": threshold,
            "consecutive_breaches": consecutive_breaches[metric],
        }
        observations.append(observation)
        if (
            consecutive_breaches[metric]
            >= POLICY["alert_after_consecutive_breaches"]
        ):
            findings.append(observation)
    return {
        "schema_version": "aidefend.accelerator-detection-result.v1",
        "control": "AID-D-005.005",
        "policy_version": POLICY["policy_version"],
        "policy_sha256": POLICY_SHA256,
        "workload_label": workload_label,
        "expected_metric_count": len(POLICY["metrics"]),
        "evaluated_metric_count": len(observations),
        "observations": observations,
        "findings": findings,
        "finding_status": "FINDING" if findings else "NO_FINDING",
    }
Step 3: Integrate with the response path

Alert to SIEM, but keep any automated kill or throttle action behind an explicit policy threshold because accelerator spikes can still be workload-driven. Log the workload label, node identity, and breached metric so responders can distinguish cryptomining, DoS, and side-channel probing.

Action: Baseline accelerator metrics under representative workloads, persist those statistics by workload class, and alert on sustained policy-threshold deviations rather than on one-off raw spikes.

Verify safely

An independent verifier must query fleet and scheduler inventories separately, replay raw metric samples with the documented aggregation method, and compare device coverage, missing intervals, baseline lookup, scores, and findings, read evidence back, verify signatures, digests, and immutability, and compare the canonical result digest.

Open-source tools

Commercial / hosted tools

Threat mappings

MITRE ATLAS

MAESTRO

OWASP LLM Top 10 2026

OWASP ML Top 10 2023

OWASP Top 10 for Agentic Applications 2026

NIST Adversarial Machine Learning 2025

Cisco Integrated AI Security and Safety Framework

Google Secure AI Framework 2.0 - Risks

Databricks AI Security Framework 3.0

AID-D-005.005: Accelerator Telemetry Anomaly Detection | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version