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.
- workloadtruth/__init__.py +1 -0
- workloadtruth/audit_log.py +148 -0
- workloadtruth/benchmark.py +111 -0
- workloadtruth/classifier/__init__.py +3 -0
- workloadtruth/classifier/experimental.py +29 -0
- workloadtruth/classifier/rules.py +183 -0
- workloadtruth/cli.py +217 -0
- workloadtruth/mcp_server.py +89 -0
- workloadtruth/telemetry/__init__.py +22 -0
- workloadtruth/telemetry/base.py +46 -0
- workloadtruth/telemetry/nvml_backend.py +56 -0
- workloadtruth/telemetry/synthetic_backend.py +151 -0
- workloadtruth/types.py +50 -0
- workloadtruth_cli-0.1.0.dist-info/METADATA +220 -0
- workloadtruth_cli-0.1.0.dist-info/RECORD +18 -0
- workloadtruth_cli-0.1.0.dist-info/WHEEL +4 -0
- workloadtruth_cli-0.1.0.dist-info/entry_points.txt +2 -0
- workloadtruth_cli-0.1.0.dist-info/licenses/LICENSE +190 -0
|
@@ -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
|
+
[](https://github.com/RudrenduPaul/WorkloadTruth/actions/workflows/ci.yml)
|
|
44
|
+
[](LICENSE)
|
|
45
|
+
[](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,,
|