workloadtruth-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,89 @@
1
+ """MCP server: exposes WorkloadTruth as agent-callable tools.
2
+
3
+ Requires the `mcp` extra (`pip install "workloadtruth-cli[mcp]"`). Started
4
+ via `workloadtruth mcp` (stdio transport), so any MCP-compatible agent
5
+ runtime can call classify_workload / run_benchmark / verify_audit_log
6
+ directly instead of shelling out to the CLI and parsing text.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from mcp.server.fastmcp import FastMCP
15
+
16
+ from workloadtruth.audit_log import append_entry, build_entry, last_hash, verify_chain
17
+ from workloadtruth.classifier.rules import classify
18
+ from workloadtruth.telemetry import get_backend
19
+
20
+ DEFAULT_LOG_PATH = Path("workloadtruth.log.jsonl")
21
+
22
+
23
+ def build_app(default_backend: str = "nvml") -> FastMCP:
24
+ app = FastMCP("workloadtruth")
25
+
26
+ @app.tool()
27
+ def classify_workload(
28
+ backend: str = default_backend,
29
+ profile: str = "training",
30
+ gpu_index: int = 0,
31
+ samples: int = 10,
32
+ interval_seconds: float = 1.0,
33
+ write_to_audit_log: bool = False,
34
+ ) -> dict[str, Any]:
35
+ """Classify the current GPU workload as TRAINING, INFERENCE, or IDLE.
36
+
37
+ `backend` is "nvml" (real hardware, requires an NVIDIA GPU + driver)
38
+ or "synthetic" (documented synthetic traces, no GPU required --
39
+ useful for testing agent integrations). `profile` only applies to
40
+ the synthetic backend ("training"/"inference"/"idle").
41
+ """
42
+ backend_kwargs = {"profile": profile} if backend == "synthetic" else {}
43
+ tb = get_backend(backend, **backend_kwargs)
44
+ try:
45
+ collected = list(tb.sample_window(gpu_index, samples, interval_seconds))
46
+ finally:
47
+ tb.close()
48
+
49
+ result = classify(collected)
50
+
51
+ if write_to_audit_log:
52
+ prev = last_hash(DEFAULT_LOG_PATH)
53
+ entry = build_entry(
54
+ timestamp=collected[-1].timestamp,
55
+ gpu_index=result.gpu_index,
56
+ workload_type=result.workload_type.value,
57
+ confidence=result.confidence,
58
+ reasons=result.reasons,
59
+ features=result.features,
60
+ prev_hash=prev,
61
+ )
62
+ append_entry(DEFAULT_LOG_PATH, entry)
63
+
64
+ return result.to_dict()
65
+
66
+ @app.tool()
67
+ def run_benchmark(trials: int = 50, window: int = 30) -> dict[str, Any]:
68
+ """Run the evasion-robustness benchmark against synthetic telemetry.
69
+
70
+ Returns per-profile accuracy under clean and evasion-obfuscated
71
+ conditions. Explicitly synthetic -- not comparable to any
72
+ real-hardware benchmark, see the returned "note" field.
73
+ """
74
+ from workloadtruth.benchmark import run_benchmark as _run_benchmark
75
+
76
+ return _run_benchmark(trials_per_cell=trials, window_size=window).to_dict()
77
+
78
+ @app.tool()
79
+ def verify_audit_log(log_file: str = str(DEFAULT_LOG_PATH)) -> dict[str, Any]:
80
+ """Verify the hash chain of the local audit log has not been tampered with."""
81
+ is_valid, message, count = verify_chain(Path(log_file))
82
+ return {"valid": is_valid, "message": message, "entries": count}
83
+
84
+ return app
85
+
86
+
87
+ def run_server(default_backend: str = "nvml") -> None:
88
+ app = build_app(default_backend=default_backend)
89
+ app.run()
@@ -0,0 +1,22 @@
1
+ from typing import Any
2
+
3
+ from workloadtruth.telemetry.base import TelemetryBackend
4
+ from workloadtruth.telemetry.synthetic_backend import SyntheticBackend
5
+
6
+ __all__ = ["TelemetryBackend", "SyntheticBackend", "get_backend"]
7
+
8
+
9
+ def get_backend(name: str, **kwargs: Any) -> TelemetryBackend:
10
+ """Resolve a telemetry backend by name.
11
+
12
+ Adding a new GPU vendor (AMD ROCm, Intel Level Zero, etc.) means
13
+ implementing TelemetryBackend and registering it here -- the classifier
14
+ and CLI never need to change.
15
+ """
16
+ if name == "synthetic":
17
+ return SyntheticBackend(**kwargs)
18
+ if name == "nvml":
19
+ from workloadtruth.telemetry.nvml_backend import NVMLBackend
20
+
21
+ return NVMLBackend(**kwargs)
22
+ raise ValueError(f"Unknown telemetry backend: {name!r}. Expected 'synthetic' or 'nvml'.")
@@ -0,0 +1,46 @@
1
+ """Telemetry backend interface.
2
+
3
+ Every GPU vendor/data-source WorkloadTruth supports implements this one
4
+ interface. The classifier and CLI only ever depend on TelemetryBackend, never
5
+ on a specific vendor's SDK -- this is what lets a new backend (AMD ROCm,
6
+ Intel Level Zero, a recorded trace file) be added without touching
7
+ classification logic.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from abc import ABC, abstractmethod
13
+ from collections.abc import Iterator
14
+
15
+ from workloadtruth.types import TelemetrySample
16
+
17
+
18
+ class TelemetryBackend(ABC):
19
+ """Yields TelemetrySample readings for one or more GPUs."""
20
+
21
+ @abstractmethod
22
+ def device_count(self) -> int:
23
+ """Number of GPUs this backend can read from."""
24
+
25
+ @abstractmethod
26
+ def sample(self, gpu_index: int) -> TelemetrySample:
27
+ """Take one instantaneous reading from the given GPU."""
28
+
29
+ def sample_window(
30
+ self, gpu_index: int, count: int, interval_seconds: float
31
+ ) -> Iterator[TelemetrySample]:
32
+ """Take `count` readings, `interval_seconds` apart.
33
+
34
+ Backends that already produce a fixed trace (e.g. SyntheticBackend in
35
+ replay mode) may override this for determinism instead of sleeping.
36
+ """
37
+ import time
38
+
39
+ for i in range(count):
40
+ yield self.sample(gpu_index)
41
+ if i < count - 1:
42
+ time.sleep(interval_seconds)
43
+
44
+ def close(self) -> None:
45
+ """Release any backend resources. Default: no-op."""
46
+ return None
@@ -0,0 +1,56 @@
1
+ """Real NVIDIA telemetry backend, via NVML (pynvml / nvidia-ml-py).
2
+
3
+ Requires the `nvml` extra (`pip install "workloadtruth-cli[nvml]"`) and an
4
+ NVIDIA driver on the host. Not exercised in CI or unit tests -- there is no
5
+ NVIDIA GPU in this project's build/test environment, so this module is
6
+ validated manually against real hardware, not covered by the test-coverage
7
+ gate (see pyproject.toml's `[tool.coverage.run]` omit list).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import time
13
+
14
+ from workloadtruth.telemetry.base import TelemetryBackend
15
+ from workloadtruth.types import TelemetrySample
16
+
17
+ try:
18
+ import pynvml
19
+ except ImportError as exc: # pragma: no cover - exercised only without the extra installed
20
+ raise ImportError(
21
+ "The NVML backend requires the 'nvml' extra: pip install \"workloadtruth-cli[nvml]\""
22
+ ) from exc
23
+
24
+
25
+ class NVMLBackend(TelemetryBackend):
26
+ def __init__(self) -> None:
27
+ pynvml.nvmlInit()
28
+ self._device_count: int = int(pynvml.nvmlDeviceGetCount())
29
+
30
+ def device_count(self) -> int:
31
+ return self._device_count
32
+
33
+ def sample(self, gpu_index: int) -> TelemetrySample:
34
+ handle = pynvml.nvmlDeviceGetHandleByIndex(gpu_index)
35
+ util = pynvml.nvmlDeviceGetUtilizationRates(handle)
36
+ mem = pynvml.nvmlDeviceGetMemoryInfo(handle)
37
+ power_mw = pynvml.nvmlDeviceGetPowerUsage(handle)
38
+ try:
39
+ processes = pynvml.nvmlDeviceGetComputeRunningProcesses(handle)
40
+ process_count = len(processes)
41
+ except pynvml.NVMLError:
42
+ process_count = 0
43
+
44
+ return TelemetrySample(
45
+ timestamp=time.time(),
46
+ gpu_index=gpu_index,
47
+ gpu_utilization_pct=float(util.gpu),
48
+ memory_utilization_pct=float(util.memory),
49
+ memory_used_mib=mem.used / (1024 * 1024),
50
+ memory_total_mib=mem.total / (1024 * 1024),
51
+ power_draw_watts=power_mw / 1000.0,
52
+ process_count=process_count,
53
+ )
54
+
55
+ def close(self) -> None:
56
+ pynvml.nvmlShutdown()
@@ -0,0 +1,151 @@
1
+ """Synthetic telemetry backend.
2
+
3
+ Generates GPU telemetry traces from documented statistical profiles instead
4
+ of reading real hardware. This exists for two honest reasons, not as a
5
+ substitute for real measurement:
6
+
7
+ 1. Unit testing the classifier without requiring NVIDIA hardware in CI.
8
+ 2. Powering `workloadtruth benchmark` on machines with no GPU (including the
9
+ machine this project was built on). Benchmark output produced by this
10
+ backend is always labeled "synthetic" -- see benchmark.py -- and must
11
+ never be presented as a live-hardware accuracy measurement.
12
+
13
+ Profile design rationale (documented so the numbers are inspectable, not
14
+ asserted):
15
+
16
+ - TRAINING: sustained high GPU utilization (large matmul-heavy forward/
17
+ backward passes keep SMs busy), memory usage grows in a step pattern
18
+ (activations + gradient buffers + optimizer state accumulate, then drop at
19
+ checkpoint boundaries), power draw stays near the sustained-load ceiling
20
+ with low variance.
21
+ - INFERENCE: bursty GPU utilization driven by request arrival (idle between
22
+ requests, a short spike to serve one), memory usage is comparatively flat
23
+ (model weights resident, no growing gradient/optimizer state), power draw
24
+ has high variance tracking the request bursts.
25
+ - IDLE: near-zero utilization and power draw, flat low memory (driver/CUDA
26
+ context overhead only).
27
+
28
+ These are simplified textbook characterizations, not fit to any real dataset
29
+ -- the honest limitation to disclose is that real-world workloads (small
30
+ inference batches with periodic re-warming, gradient-accumulation training
31
+ with long low-utilization gaps, mixed inference+LoRA-finetuning servers) can
32
+ blur these boundaries. The classifier's rule thresholds and this generator's
33
+ profiles are documented in the same place (this file and rules.py) so both
34
+ can be inspected and challenged together.
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import random
40
+
41
+ from workloadtruth.telemetry.base import TelemetryBackend
42
+ from workloadtruth.types import TelemetrySample
43
+
44
+ _PROFILES = {
45
+ "training": {
46
+ "gpu_util_mean": 88.0,
47
+ "gpu_util_std": 4.0,
48
+ "mem_util_mean": 70.0,
49
+ "mem_util_std": 6.0,
50
+ "mem_base_mib": 18000.0,
51
+ "mem_growth_mib_per_sample": 120.0,
52
+ "mem_ceiling_mib": 38000.0,
53
+ "power_mean_watts": 340.0,
54
+ "power_std_watts": 12.0,
55
+ "process_count": 1,
56
+ },
57
+ "inference": {
58
+ "gpu_util_mean": 35.0,
59
+ "gpu_util_std": 28.0,
60
+ "mem_util_mean": 22.0,
61
+ "mem_util_std": 5.0,
62
+ "mem_base_mib": 9000.0,
63
+ "mem_growth_mib_per_sample": 0.0,
64
+ "mem_ceiling_mib": 9500.0,
65
+ "power_mean_watts": 160.0,
66
+ "power_std_watts": 55.0,
67
+ "process_count": 1,
68
+ },
69
+ "idle": {
70
+ "gpu_util_mean": 1.0,
71
+ "gpu_util_std": 1.0,
72
+ "mem_util_mean": 0.5,
73
+ "mem_util_std": 0.5,
74
+ "mem_base_mib": 400.0,
75
+ "mem_growth_mib_per_sample": 0.0,
76
+ "mem_ceiling_mib": 400.0,
77
+ "power_mean_watts": 35.0,
78
+ "power_std_watts": 5.0,
79
+ "process_count": 0,
80
+ },
81
+ }
82
+
83
+ MEMORY_TOTAL_MIB = 40960.0 # A100 40GB, used as the fixed reference card
84
+
85
+
86
+ def _clamp(value: float, low: float, high: float) -> float:
87
+ return max(low, min(high, value))
88
+
89
+
90
+ class SyntheticBackend(TelemetryBackend):
91
+ """Generates telemetry from a named profile ("training"/"inference"/"idle").
92
+
93
+ `evasion` applies a documented obfuscation transform meant to defeat a
94
+ naive classifier: it caps utilization swings, injects artificial idle
95
+ gaps into a training profile, and flattens memory growth -- modeling an
96
+ operator deliberately trying to make a training job telemetry-resemble
97
+ inference. This is what `workloadtruth benchmark` uses to measure the
98
+ classifier's evasion-robustness gap, the same property arXiv:2606.19262
99
+ reports a real accuracy drop against (43-87%) -- WorkloadTruth's own
100
+ number, measured against this synthetic evasion transform, is reported
101
+ separately in the README and must never be presented as the same
102
+ measurement.
103
+ """
104
+
105
+ def __init__(self, profile: str = "training", evasion: bool = False, seed: int | None = None):
106
+ if profile not in _PROFILES:
107
+ raise ValueError(
108
+ f"Unknown synthetic profile: {profile!r}. Expected one of {list(_PROFILES)}."
109
+ )
110
+ self.profile_name = profile
111
+ self.evasion = evasion
112
+ self._rng = random.Random(seed)
113
+ self._sample_index = 0
114
+ self._t0 = 0.0
115
+
116
+ def device_count(self) -> int:
117
+ return 1
118
+
119
+ def sample(self, gpu_index: int = 0) -> TelemetrySample:
120
+ p = _PROFILES[self.profile_name]
121
+ gpu_util = self._rng.gauss(p["gpu_util_mean"], p["gpu_util_std"])
122
+ mem_util = self._rng.gauss(p["mem_util_mean"], p["mem_util_std"])
123
+ power = self._rng.gauss(p["power_mean_watts"], p["power_std_watts"])
124
+ mem_used = p["mem_base_mib"] + p["mem_growth_mib_per_sample"] * self._sample_index
125
+ mem_used = min(mem_used, p["mem_ceiling_mib"])
126
+ process_count = int(p["process_count"])
127
+
128
+ if self.evasion:
129
+ # Cap utilization swings and flatten memory growth to mimic an
130
+ # operator deliberately disguising a training job as inference.
131
+ gpu_util = _clamp(gpu_util, 0.0, 55.0)
132
+ mem_used = min(mem_used, p["mem_base_mib"] * 1.1)
133
+ if self._sample_index % 4 == 0:
134
+ gpu_util *= 0.2 # inject an artificial idle gap
135
+
136
+ self._sample_index += 1
137
+ timestamp = self._t0 + self._sample_index
138
+
139
+ return TelemetrySample(
140
+ timestamp=timestamp,
141
+ gpu_index=gpu_index,
142
+ gpu_utilization_pct=_clamp(gpu_util, 0.0, 100.0),
143
+ memory_utilization_pct=_clamp(mem_util, 0.0, 100.0),
144
+ memory_used_mib=_clamp(mem_used, 0.0, MEMORY_TOTAL_MIB),
145
+ memory_total_mib=MEMORY_TOTAL_MIB,
146
+ power_draw_watts=max(0.0, power),
147
+ process_count=process_count,
148
+ )
149
+
150
+ def close(self) -> None:
151
+ return None
workloadtruth/types.py ADDED
@@ -0,0 +1,50 @@
1
+ """Shared types for WorkloadTruth."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+ from typing import Any
8
+
9
+
10
+ class WorkloadType(str, Enum):
11
+ TRAINING = "TRAINING"
12
+ INFERENCE = "INFERENCE"
13
+ IDLE = "IDLE"
14
+ UNKNOWN = "UNKNOWN"
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class TelemetrySample:
19
+ """One point-in-time reading from a GPU telemetry backend."""
20
+
21
+ timestamp: float
22
+ gpu_index: int
23
+ gpu_utilization_pct: float
24
+ memory_utilization_pct: float
25
+ memory_used_mib: float
26
+ memory_total_mib: float
27
+ power_draw_watts: float
28
+ process_count: int
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class ClassificationResult:
33
+ workload_type: WorkloadType
34
+ confidence: float
35
+ gpu_index: int
36
+ window_seconds: float
37
+ sample_count: int
38
+ reasons: list[str] = field(default_factory=list)
39
+ features: dict[str, float] = field(default_factory=dict)
40
+
41
+ def to_dict(self) -> dict[str, Any]:
42
+ return {
43
+ "workload_type": self.workload_type.value,
44
+ "confidence": round(self.confidence, 4),
45
+ "gpu_index": self.gpu_index,
46
+ "window_seconds": self.window_seconds,
47
+ "sample_count": self.sample_count,
48
+ "reasons": self.reasons,
49
+ "features": {k: round(v, 4) for k, v in self.features.items()},
50
+ }
@@ -0,0 +1,220 @@
1
+ Metadata-Version: 2.4
2
+ Name: workloadtruth-cli
3
+ Version: 0.1.0
4
+ Summary: Classify a GPU workload as inference or training from telemetry alone, no application changes required.
5
+ Project-URL: Homepage, https://github.com/RudrenduPaul/WorkloadTruth
6
+ Project-URL: Repository, https://github.com/RudrenduPaul/WorkloadTruth
7
+ Project-URL: Issues, https://github.com/RudrenduPaul/WorkloadTruth/issues
8
+ Project-URL: Changelog, https://github.com/RudrenduPaul/WorkloadTruth/blob/main/CHANGELOG.md
9
+ Author: Rudrendu Paul
10
+ License-Expression: Apache-2.0
11
+ License-File: LICENSE
12
+ Keywords: agent-tools,ai-governance,compute-monitoring,cuda,finops,gpu,mcp,mlops,nvml,observability
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: System Administrators
17
+ Classifier: License :: OSI Approved :: Apache Software License
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Topic :: System :: Hardware
24
+ Classifier: Topic :: System :: Monitoring
25
+ Requires-Python: >=3.9
26
+ Requires-Dist: click>=8.1
27
+ Provides-Extra: dev
28
+ Requires-Dist: build>=1.2; extra == 'dev'
29
+ Requires-Dist: mypy>=1.10; extra == 'dev'
30
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
31
+ Requires-Dist: pytest>=8.0; extra == 'dev'
32
+ Requires-Dist: ruff>=0.6; extra == 'dev'
33
+ Provides-Extra: mcp
34
+ Requires-Dist: mcp>=1.2; extra == 'mcp'
35
+ Provides-Extra: nvml
36
+ Requires-Dist: nvidia-ml-py>=12.560; extra == 'nvml'
37
+ Description-Content-Type: text/markdown
38
+
39
+ # WorkloadTruth
40
+
41
+ **Classify a GPU workload as `TRAINING`, `INFERENCE`, or `IDLE` from telemetry alone. No code changes to the workload, no self-reported job labels.**
42
+
43
+ [![CI](https://github.com/RudrenduPaul/WorkloadTruth/actions/workflows/ci.yml/badge.svg)](https://github.com/RudrenduPaul/WorkloadTruth/actions/workflows/ci.yml)
44
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
45
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9%2B-blue)](pyproject.toml)
46
+
47
+ Every GPU scheduler in common use today, including [run:ai](https://docs.run.ai/v2.20/Researcher/workloads/inference/inference-overview/), Slurm, and Kubernetes GPU operators, asks you to *declare* whether a job is training or inference at submission time. None of them check. WorkloadTruth reads GPU telemetry (utilization, memory pattern, power draw) and answers the question independently, so a mislabeled or misbehaving job doesn't go unnoticed.
48
+
49
+ ## Quick summary
50
+
51
+ - **Install:** `pip install "workloadtruth-cli[nvml]"` for real GPU access, or `pip install workloadtruth-cli` to try it with the synthetic backend, no GPU needed
52
+ - **Use it for:** catching cost-misallocated GPU jobs (a job billed as low-priority "inference" that's actually running full training) and unauthorized workload changes (an inference endpoint that starts training on live traffic without sign-off)
53
+ - **What it's not:** a compliance or regulatory-audit tool. No regulation currently requires this kind of monitoring, see [What WorkloadTruth is not](#what-workloadtruth-is-not) below
54
+ - **Prior art:** builds on and cites [arXiv:2606.19262](https://arxiv.org/abs/2606.19262) (ICML 2026), see [Relationship to prior research](#relationship-to-prior-research)
55
+
56
+ ## Install
57
+
58
+ ```bash
59
+ # Real NVIDIA GPU telemetry (requires an NVIDIA driver on the host)
60
+ pip install "workloadtruth-cli[nvml]"
61
+
62
+ # Try it without a GPU, using the synthetic backend
63
+ pip install workloadtruth-cli
64
+
65
+ # npm launcher (thin wrapper around the PyPI package, see "Why two registries")
66
+ npx workloadtruth-cli --help
67
+ ```
68
+
69
+ ## Quickstart
70
+
71
+ ```bash
72
+ # No GPU required. Classify a synthetic "training" telemetry trace.
73
+ $ workloadtruth classify --backend synthetic --profile training --samples 10 --interval 0
74
+ workload_type : TRAINING
75
+ confidence : 1.00
76
+ gpu_index : 0
77
+ samples : 10 over 9.0s
78
+ reasons:
79
+ - avg GPU utilization 87.8% >= training threshold 65.0%
80
+ - low GPU utilization variance (std=3.4) <= training ceiling 15.0
81
+ - memory growing 120.0 MiB/sample >= training threshold 5.0
82
+ - low power-draw variance (std=9.4W) <= training ceiling 25.0W
83
+
84
+ # Real hardware
85
+ $ workloadtruth classify --backend nvml --samples 10 --interval 1 --json
86
+ ```
87
+
88
+ `--json` on every command switches to machine-readable output for scripts and agents.
89
+
90
+ ## How classification works
91
+
92
+ WorkloadTruth ships a **rule-based classifier only** in v0.1: a set of documented, inspectable thresholds over four features extracted from a telemetry window (average and variance of GPU utilization, memory-growth slope, average and variance of power draw). Every threshold lives as a named constant in [`src/workloadtruth/classifier/rules.py`](src/workloadtruth/classifier/rules.py) with a comment explaining its intuition. Nothing is a black box.
93
+
94
+ `--experimental` (an ML-based classifier) is present as a flag but fails loudly with an explanation rather than shipping a fake result. [arXiv:2606.19262](https://arxiv.org/abs/2606.19262)'s trained model and dataset were never published, and this project has no NVIDIA GPU in its build environment to collect real training data. An ML classifier ships here only once it's trained on a real, disclosed dataset and independently shown to beat the rule-based baseline, not before.
95
+
96
+ ## Benchmark
97
+
98
+ `workloadtruth benchmark` measures the classifier's accuracy against documented synthetic GPU telemetry traces (see [`synthetic_backend.py`](src/workloadtruth/telemetry/synthetic_backend.py) for the exact profile parameters), both clean and under a deliberate evasion transform that mimics an operator disguising a training job as inference (capping utilization swings, flattening memory growth, injecting artificial idle gaps).
99
+
100
+ **This is run on synthetic data, not live NVIDIA hardware, so it is not directly comparable to arXiv:2606.19262's real-hardware numbers.** Both are reported below, side by side, never blended into one figure.
101
+
102
+ | | arXiv:2606.19262 (real hardware, NVML) | WorkloadTruth v0.1 (synthetic traces, 300 trials/cell) |
103
+ |---|---|---|
104
+ | Clean accuracy | 98.2% | 100.0% |
105
+ | Evasion accuracy | 43-87% | 66.7% overall |
106
+
107
+ The overall evasion number hides the real finding. Broken out by workload type, WorkloadTruth's rule-based classifier is currently **completely fooled on evasive training workloads (0% accuracy)**, while staying accurate on evasive inference and idle traces (100% each). Capping GPU utilization alone is enough to make a training job read as inference to this classifier today.
108
+
109
+ | Profile | Clean accuracy | Evasion accuracy |
110
+ |---|---|---|
111
+ | Training | 100.0% | **0.0%** |
112
+ | Inference | 100.0% | 100.0% |
113
+ | Idle | 100.0% | 100.0% |
114
+
115
+ This is the concrete, disclosed gap the benchmark suite exists to surface and track, not a footnote. A rule-based classifier that only looks at utilization variance is trivially evadable. Closing this gap, through multi-signal fusion or a real trained classifier once data exists, is the roadmap, not a solved problem. Reproduce it yourself:
116
+
117
+ ```bash
118
+ workloadtruth benchmark --trials 300 --window 30 --json
119
+ ```
120
+
121
+ ## CLI reference
122
+
123
+ ```
124
+ $ workloadtruth --help
125
+ Usage: workloadtruth [OPTIONS] COMMAND [ARGS]...
126
+
127
+ Classify a GPU workload as INFERENCE, TRAINING, or IDLE from telemetry
128
+ alone.
129
+
130
+ Options:
131
+ --version Show the version and exit.
132
+ --help Show this message and exit.
133
+
134
+ Commands:
135
+ benchmark Run the evasion-robustness benchmark against synthetic...
136
+ classify One-shot classification of the current GPU workload.
137
+ mcp Start an MCP server exposing classify/benchmark/verify-log...
138
+ verify-log Verify the hash chain of a local audit log has not been...
139
+ watch Continuously classify and append hash-chained entries to...
140
+ ```
141
+
142
+ | Command | Purpose |
143
+ |---|---|
144
+ | `classify` | One-shot classification. `--backend synthetic\|nvml`, `--json` for machine-readable output. |
145
+ | `watch` | Continuous classification; appends a hash-chained entry to a local audit log on every window. |
146
+ | `benchmark` | Runs the evasion-robustness benchmark (see above). |
147
+ | `verify-log` | Re-derives every audit-log entry's hash and confirms the chain hasn't been tampered with. |
148
+ | `mcp` | Starts an MCP server (stdio) exposing `classify_workload`, `run_benchmark`, `verify_audit_log` as agent-callable tools. Requires `pip install "workloadtruth-cli[mcp]"`. |
149
+
150
+ Every command supports `--json`. Full flag reference: `workloadtruth <command> --help`.
151
+
152
+ ## Agent-native (MCP + A2A)
153
+
154
+ ```bash
155
+ pip install "workloadtruth-cli[mcp]"
156
+ workloadtruth mcp
157
+ ```
158
+
159
+ Exposes three tools over stdio MCP: `classify_workload`, `run_benchmark`, `verify_audit_log`. A `.well-known/agent.json` manifest is shipped at the repo root for A2A-style discovery, listing both the CLI and MCP interfaces and the packages that provide them.
160
+
161
+ ## Audit log
162
+
163
+ `workloadtruth watch` appends a hash-chained entry to `workloadtruth.log.jsonl` on every classification window. Each entry's hash covers its own content plus the previous entry's hash, so any edit, reorder, or deletion after the fact breaks the chain from that point forward. `workloadtruth verify-log` re-derives every hash and reports the first broken link, if any.
164
+
165
+ This proves what was classified, when, and that the local record hasn't been silently altered afterward. **It does not prove the classification itself was correct**, and it is not evidence of regulatory compliance. See below.
166
+
167
+ ## Why two registries
168
+
169
+ WorkloadTruth's implementation is Python. NVML access (`pynvml`/`nvidia-ml-py`) is the mature, official way to read NVIDIA GPU telemetry, and it's also what the closest prior art ([arXiv:2606.19262](https://arxiv.org/abs/2606.19262)) uses. The npm package (`workloadtruth-cli`) is a thin launcher, not a reimplementation. It locates and execs the real `workloadtruth` binary installed from PyPI, so `npx workloadtruth-cli` works for npm-first agent tooling without duplicating the classifier in two languages.
170
+
171
+ ## Comparison
172
+
173
+ | | WorkloadTruth | NVIDIA DCGM / `dcgm-exporter` | run:ai | Weights & Biases |
174
+ |---|---|---|---|---|
175
+ | Reads GPU telemetry | Yes (via NVML) | Yes (source) | Yes | Yes |
176
+ | Classifies workload type automatically | **Yes** | No, exposes raw metrics only | No, workload type is user-declared at job submission | No, training-run-scoped by design, no classification |
177
+ | Local, hash-chained audit trail | Yes | No | No | No |
178
+ | Evasion-robustness benchmark | Yes (documented, reproducible) | N/A | N/A | N/A |
179
+ | Requires an NVIDIA GPU | Only for the `nvml` backend; the `synthetic` backend works without one | Yes | Yes | No (general system metrics) |
180
+
181
+ Checked directly against each project's own documentation: [DCGM exporter docs](https://docs.nvidia.com/datacenter/dcgm/latest/gpu-telemetry/dcgm-exporter.html), [run:ai inference overview](https://docs.run.ai/v2.20/Researcher/workloads/inference/inference-overview/), [W&B system metrics docs](https://docs.wandb.ai/models/ref/python/experiments/system-metrics). None of these classify workload type from telemetry alone. That gap is what WorkloadTruth fills.
182
+
183
+ ## Relationship to prior research
184
+
185
+ WorkloadTruth's core technique, classifying training vs. non-training GPU activity from telemetry, is not novel. It's the direct application of a real, active research thread:
186
+
187
+ 1. Yonadav Shavit (Harvard), [*"What does it take to catch a Chinchilla?"*](https://arxiv.org/abs/2303.11341) (2023): proposed hardware-level "training transcripts" for verifying large training runs.
188
+ 2. GovAI, [*"Computing Power and the Governance of AI"*](https://www.governance.ai/analysis/computing-power-and-the-governance-of-ai) (2024): surveyed compute-governance mechanisms, explicitly framed as exploratory, not endorsed policy.
189
+ 3. [*"Hardware-Enabled Mechanisms for Verifying Responsible AI Development"*](https://arxiv.org/abs/2505.03742) (2025): hardware-security researchers proposing on-chip attestation.
190
+ 4. Rahman & Tajdari, [*"Detecting Hidden ML Training With Zero-Overhead Telemetry"*](https://arxiv.org/abs/2606.19262) (ICML 2026 Technical AI Governance workshop): a working NVML-telemetry classifier, 98.2% accurate on unobfuscated workloads, the direct prior art for this project's core classification technique.
191
+
192
+ **What WorkloadTruth adds:** as of this project's own research (2026-07-19), no open-source, installable implementation of this research thread existed, only academic prototypes. WorkloadTruth is that packaging: a real CLI, an MCP server, a hash-chained audit log, and a reproducible evasion-robustness benchmark, built in the open. It does not claim to improve on the paper's classification technique. See the benchmark section above: the current rule-based classifier is considerably more evadable than the paper's ML approach on the one axis it measures.
193
+
194
+ ## What WorkloadTruth is not
195
+
196
+ - **Not a compliance or regulatory-audit tool.** No law currently requires inference/training classification or reporting. Any future claim otherwise will name the specific enacted regulation; none exists as of this writing.
197
+ - **Not a content inspector.** WorkloadTruth reads GPU-level signals only (utilization, memory, power). It never inspects model weights, training data, prompts, or completions.
198
+ - **Not proof of "compliant" or "safe" operation.** The audit log proves what was classified and when, and that the record wasn't altered afterward, not that the classification was correct or that any policy was followed.
199
+
200
+ ## FAQ
201
+
202
+ **Does this need an NVIDIA GPU?**
203
+ Only for the `nvml` backend. `--backend synthetic` runs the full classifier and CLI against documented synthetic traces, no GPU required. Useful for trying the tool or for CI.
204
+
205
+ **Can it classify AMD or Intel GPU workloads?**
206
+ Not in v0.1. The telemetry layer is a pluggable interface (`TelemetryBackend`) specifically so a new vendor backend (AMD ROCm, Intel Level Zero) can be added without touching the classifier. See [CONTRIBUTING.md](CONTRIBUTING.md).
207
+
208
+ **Is the classifier accurate enough to bill or penalize someone based on its output?**
209
+ Not yet, and the benchmark section above is the honest reason why: 0% accuracy on evasive training workloads today. Treat `workload_type` as a signal to investigate, not a verdict.
210
+
211
+ **Why not just use the ML classifier from the paper?**
212
+ Its trained weights and dataset were never published. Reimplementing an ML classifier without real training data would produce an unvalidated accuracy claim, not a measured one. See [How classification works](#how-classification-works).
213
+
214
+ ## Contributing
215
+
216
+ See [CONTRIBUTING.md](CONTRIBUTING.md). Security issues: see [SECURITY.md](SECURITY.md).
217
+
218
+ ## License
219
+
220
+ [Apache 2.0](LICENSE)
@@ -0,0 +1,18 @@
1
+ workloadtruth/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
2
+ workloadtruth/audit_log.py,sha256=AEzgA1pu42rLe3ERdNXBxgTK2K5cnHax9M1Rxtfim8E,4786
3
+ workloadtruth/benchmark.py,sha256=xdIUuDaLVLFw6ED1_qsKH-7CeDrVMLbrxR2jtJuUDFE,4113
4
+ workloadtruth/cli.py,sha256=tZLTJmv3E-KOuczbDmhSVN9F8UQ3ExyCQUkOuiXNeKY,7113
5
+ workloadtruth/mcp_server.py,sha256=OztucxHqKMVt1tCX63CCJA99z169BCQGifrtdZGtL2Y,3302
6
+ workloadtruth/types.py,sha256=Ow_sBBaR4F_0pfomCgjDe8WkWMzRFIPolgKk_XZEKa4,1335
7
+ workloadtruth/classifier/__init__.py,sha256=HoVpv_hgZ9tSR1Vx3T4g8xV2qdEvjYJQxTMgvgCNwXU,114
8
+ workloadtruth/classifier/experimental.py,sha256=k8nAjxRb_-4XQXA7gju9mUary_A80M_2VTJS61774rU,1210
9
+ workloadtruth/classifier/rules.py,sha256=P-PJjspMGjA5HyekyazA542UhPhLLa9DSWzhG_lKyMw,7308
10
+ workloadtruth/telemetry/__init__.py,sha256=qdOZGPY8Z00dnouojwcOobgVY4deVfaEmkYMJypno5M,811
11
+ workloadtruth/telemetry/base.py,sha256=1mtmhISGLQNyaeNgwoaiebnYDTY7KHELJ4O0NPXCxpg,1491
12
+ workloadtruth/telemetry/nvml_backend.py,sha256=FDChP60SWxOwhYozNZ6cmE2cMcbAUkLTRqmOuqR2oGE,2029
13
+ workloadtruth/telemetry/synthetic_backend.py,sha256=0w1pdcA6DKMGIHyZmdMmUT8mk-qiaxyc1dWP5EXn9uA,6049
14
+ workloadtruth_cli-0.1.0.dist-info/METADATA,sha256=cuOFBHfMlHtlOVCRn61qdlej-4gTGOc3M4Hs9iVeWDg,14766
15
+ workloadtruth_cli-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
16
+ workloadtruth_cli-0.1.0.dist-info/entry_points.txt,sha256=WiGvdk4T12N13puDy9-1l6TShdQpNpkQ-2KFzXNjLnc,57
17
+ workloadtruth_cli-0.1.0.dist-info/licenses/LICENSE,sha256=vWCQIFSDq8K7yxYvTAwPAT7d5PG2Czswa2NQnJbNw74,10758
18
+ workloadtruth_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ workloadtruth = workloadtruth.cli:main