Adding a Hardware Accelerator Backend to warpt
Overview
warpt is meant to be a vendor-agnostic package for power measurement, hardware configuration detection, and stress testing. New hardware support can be added by implementing three classes in warpt:
- Accelerator Backend — The accelerator backend exposes device discovery, memory, utilization, identity, and basic telemetry. This is the core requirement for integration with warpt.
- Power Backend — Integrates power draw, temperature, and energy telemetry per device. Enables warpt's power profiling capabilities.
- Stress Backend (Optional but Recommended) — Allows the implementation of stress test workloads so warpt can apply standardized load to your hardware.
An "accelerator" may be a GPU, TPU, NPU, or any other compute accelerator supported by your platform. The sections below walk through each backend type, the required methods, and concrete examples.
Integration Checklist
| Step | File(s) | Done |
|---|---|---|
| 1. Implement AcceleratorBackend subclass | warpt/backends/yourvendor.py | ☐ |
| 2. Add factory registration | warpt/backends/factory.py | ☐ |
| 3. Add package to setuptools | pyproject.toml | ☐ |
| 4. Add optional dependency group | pyproject.toml | ☐ |
| 5. Write factory fallthrough test | tests/test_backends_factory.py | ☐ |
| 6. Write backend unit tests (mocked) | tests/test_yourvendor_backend.py | ☐ |
| 7. Verify list_devices() returns valid GPUInfo | Manual test with hardware | ☐ |
| 8. Verify stress tests run on your device | warpt stress -c accelerator | ☐ |
| 9. Verify power readings work | warpt list (check power column) | ☐ |
| 10. (Optional) Implement PowerBackend | warpt/backends/power/yourvendor_power.py | ☐ |
| 11. (Optional) Add custom stress tests | warpt/stress/yourvendor_compute.py | ☐ |
| 12. Submit PR with tests passing | All files | ☐ |
Architecture Overview
The following diagram shows how warpt's layered architecture separates hardware-specific code from the core logic. Your backend sits at the bottom of this stack:
CLI Commands (warpt stress, warpt list, warpt profile)
│
Stress Tests / Profiling Engine / Report Generation
│
Backend Abstraction Layer (AcceleratorBackend ABC)
│
┌───────────────┬───────────────┬──────────────────┐
│ NvidiaBackend │ (empty) │ YourBackend │
│ (pynvml) │ │ (your SDK) │
└───────────────┴───────────────┴──────────────────┘
The core never imports vendor-specific libraries directly. All hardware interaction is mediated through the backend abstraction, enabling warpt to support heterogeneous multi-vendor environments.
Note: Only the NVIDIA backend is currently shipped as a complete reference implementation. We use the NvidiaBackend as an example throughout this document. We encourage users to refer to its implementation for guidance.
Source Code Organization
warpt/
├── backends/
│ ├── base.py # AcceleratorBackend Abstract Base Class (ABC)
│ ├── factory.py # Add your try/except block here
│ ├── nvidia.py # Reference implementation
│ ├── yourvendor.py # Add this: Implements the AcceleratorBackend ABC
│ └── power/
│ ├── base.py # PowerBackend ABC
│ ├── factory.py # Add your power init here
│ ├── nvidia_power.py # Reference
│ └── yourvendor_power.py # Add this for power readings (optional, but recommended)
├── stress/
│ ├── base.py # StressTest ABC
│ ├── gpu_compute.py
│ └── yourvendor_compute.py # Add this to enable stress testing on your hardware
├── models/
│ ├── list_models.py # GPUInfo (Pydantic) — use extra_metrics
│ ├── power_models.py # Power data contracts (@dataclass)
│ └── profile_models.py # Profile schemas (Pydantic)
1. AcceleratorBackend
The accelerator backend is the only required abstract class that must be implemented to enable basic functionality with warpt. Implementing this class enables the collection of hardware information and basic telemetry from your hardware.
1.1 Responsibilities
- Device discovery and enumeration
- Device identity (model, UUID, PCIe generation, driver version)
- Memory reporting (total, used, free)
- Utilization reporting (compute %, memory %)
- Temperature reading
- Basic power reading (point-in-time wattage)
- Throttle reason reporting
- PyTorch device string mapping for stress test workloads
1.2 The AcceleratorBackend Interface
Every accelerator backend must implement the AcceleratorBackend Abstract Base Class defined in warpt/backends/base.py. This abstract base class defines the complete contract between warpt's core and your hardware SDK.
Why "Accelerator Backend"? warpt uses the term "accelerator" rather than "GPU" because the backend interface is designed to support a variety of compute accelerators — GPUs, TPUs, NPUs, or custom ASICs.
1.3 Full Method Reference
The following table lists every abstract method you must implement. All methods should handle vendor SDK errors gracefully and return None where indicated rather than raising exceptions.
| Method | Returns | Purpose |
|---|---|---|
__init__(self) |
None |
Initialize your vendor SDK (e.g., nvmlInit). Called once when the backend is instantiated. |
is_available(self) |
bool |
Return True if at least one of your devices is present and accessible on this system. |
get_device_count(self) |
int |
Return the number of your accelerator devices detected on this system. |
list_devices(self) |
list[GPUInfo] |
Return full device info for every detected device. See GPUInfo schema below. |
get_temperature(self, index) |
float or None |
Return the current temperature in Celsius for the device at the given index. |
get_memory_usage(self, index) |
dict or None |
Return {total, used, free} in bytes for device memory. |
get_utilization(self, index) |
dict or None |
Return {gpu: 0–100, memory: 0–100} utilization percentages. |
get_pytorch_device_string(self, device_id) |
str |
Return the PyTorch device string (e.g., "cuda:0", "xpu:0", "tt:0"). |
get_power_usage(self, index) |
float or None |
Return current power draw in Watts for the device at the given index. |
get_throttle_reasons(self, index) |
list[str] |
Return active throttle reasons: ["thermal", "power_limit", ...]. Empty list if none. |
get_driver_version(self) |
str or None |
Return the vendor driver version string. |
shutdown(self) |
None |
Clean up vendor SDK resources. Called when warpt exits. |
1.4 GPUInfo Data Contract
Defined in warpt/models/list_models.py. This is the Pydantic model returned by list_devices():
class GPUInfo(BaseModel):
index: int # 0-based device index
model: str # e.g., "NVIDIA RTX 4090"
memory_gb: int # Total device memory in GB
uuid: str | None # Persistent unique device ID
compute_capability: str | None # e.g., "8.9" (None if N/A)
pcie_gen: int | None # PCIe generation (3, 4, 5)
driver_version: str | None # Vendor driver version
extra_metrics: dict[str, Any] | None # Vendor-specific extras
The extra_metrics field is your extension point. Use it for vendor-specific data that doesn't map to the standard fields (e.g., board type, firmware version, ethernet port count, ASIC temperature breakdown). The profiling engine stores this data alongside the standard fields.
Note on UUIDs (Highly Recommended): If your platform exposes a stable, unique device identifier, we recommend populating the
uuidfield. A persistent hardware ID allows warpt to more accurately track the number and identity of accelerators in a system and enables future features such as per-device telemetry. If your platform does not provide a stable identifier, returnNone.
1.5 Minimal Implementation Skeleton
Replace the placeholders with your vendor SDK calls:
# warpt/backends/yourvendor.py
from warpt.backends.base import AcceleratorBackend
from warpt.models.list_models import GPUInfo
class YourVendorBackend(AcceleratorBackend):
"""Backend for YourVendor accelerators."""
def __init__(self):
# Initialize your vendor SDK here.
# This is called once when the backend is first created.
# Equivalent to pynvml.nvmlInit() in the NVIDIA backend.
your_sdk.init()
def is_available(self) -> bool:
# Return True if at least one device is accessible.
# The factory calls this to decide whether to use your backend.
return self.get_device_count() > 0
def get_device_count(self) -> int:
# Query your SDK for the number of detected devices.
return your_sdk.device_count()
def list_devices(self) -> list[GPUInfo]:
# Build a GPUInfo object for each device.
# Use extra_metrics for any vendor-specific data that
# doesn't map to the standard GPUInfo fields.
devices = []
for i in range(self.get_device_count()):
devices.append(GPUInfo(
index=i,
model="YourVendor Accelerator",
memory_gb=your_sdk.get_memory(i) // (1024**3),
uuid=your_sdk.get_uuid(i),
compute_capability=None,
pcie_gen=your_sdk.get_pcie_gen(i),
driver_version=self.get_driver_version(),
extra_metrics={
"board_type": your_sdk.get_board_type(i),
"firmware_version": your_sdk.get_fw_ver(i),
},
))
return devices
def get_temperature(self, index: int) -> float | None:
return your_sdk.get_temp(index)
def get_memory_usage(self, index: int) -> dict | None:
return {"total": ..., "used": ..., "free": ...}
def get_utilization(self, index: int) -> dict | None:
return {"gpu": ..., "memory": ...}
def get_pytorch_device_string(self, device_id: int) -> str:
# Must match what PyTorch uses for your hardware.
return f"your_device:{device_id}"
def get_power_usage(self, index: int) -> float | None:
# Return power in Watts. Convert if your SDK
# reports in milliwatts (like NVIDIA does).
return your_sdk.get_power(index)
def get_throttle_reasons(self, index: int) -> list[str]:
return [] # Implement if your SDK exposes this
def get_driver_version(self) -> str | None:
return your_sdk.get_driver_version()
def shutdown(self):
your_sdk.cleanup()
1.6 Defining a Custom Device Info Model
For most backends, GPUInfo with extra_metrics is sufficient. You can store any vendor-specific data in that dict field and it will be persisted alongside the standard fields. No new model required.
However, if your hardware has fundamental properties that don't map to any existing GPUInfo field — for example, a TPU with tpu_version and mesh_topology, or a quantum backend with qubit_count and coherence_time_us — you should define a custom device info model. Typed fields give you validation, autocompletion, and self-documenting schemas, which are better than opaque dicts for anything that's core to your hardware's identity.
To define a custom model:
- Subclass
GPUInfo(or a futureDeviceInfobase) inwarpt/models/list_models.py:
class TPUInfo(GPUInfo):
tpu_version: int
mesh_topology: str
hbm_bandwidth_gbps: float | None = None
Register it in
warpt/models/__init__.pyand add it to the__all__export.Update
HardwareInfoinwarpt/models/list_models.pyto accept your new model type.In your backend subclass, you can type
list_devices()as returninglist[TPUInfo]instead oflist[GPUInfo]. This works becauseTPUInfois a subclass ofGPUInfo, so it has all the same fields plus your extras. The rest of warpt treats them as normalGPUInfoobjects and continues to work without modification.
Only define a custom model when extra_metrics genuinely falls short. If you find yourself putting the same keys into extra_metrics on every single device and validating them manually, that's a signal to promote them to typed fields on a proper subclass.
2. Power Backend
The power backend provides advanced continuous power monitoring beyond the simple get_power_usage() method on the accelerator backend. It is separate from the accelerator backend and optional, but required for vendors who want full warpt power profiling support.
2.1 Responsibilities
- Continuous power telemetry (not just point-in-time)
- Domain-level power breakdown (GPU core, DRAM, voltage regulator, etc.)
- Energy accumulation (joules over time)
- Correlated thermal data (temperature alongside power readings)
2.2 How It Differs from get_power_usage()
| get_power_usage() (Accelerator Backend) | Power Backend | |
|---|---|---|
| Scope | Single wattage reading | Full domain breakdown |
| Frequency | On-demand, point-in-time | Continuous sampling |
| Data | float (watts) | PowerSnapshot with domains, processes, metadata |
| Required? | Yes (part of AcceleratorBackend) | Optional |
2.3 Power Data Contracts
Defined in warpt/models/power_models.py. Note: these are plain Python @dataclass classes, not Pydantic models — they do not have .model_dump() or .model_validate() methods.
@dataclass
class GPUPowerInfo:
index: int # Device index
name: str # Device model name
power_watts: float # Current power draw
power_limit_watts: float | None # TDP / power cap
utilization_percent: float # Compute utilization
memory_utilization_percent: float # Memory utilization
temperature_celsius: float | None # Current temp
processes: list[dict[str, Any]] # Running processes
metadata: dict[str, Any] # Vendor extras
@dataclass
class PowerSnapshot:
timestamp: float # Unix timestamp
total_power_watts: float | None # System total
domains: list[DomainPower] # Per-domain breakdown
gpus: list[GPUPowerInfo] # Per-device readings
processes: list[ProcessPower] # Per-process power
platform: str # OS identifier
available_sources: list[PowerSource] # Active sources
2.4 Implementation
Your backend should subclass PowerBackend defined in warpt/backends/power/base.py. See warpt/backends/power/nvidia_power.py as the reference implementation. Your file goes in:
warpt/backends/power/yourvendor_power.py
Register it in warpt/backends/power/factory.py by adding your power source to the PowerMonitor.initialize() method.
3. Stress Backend
warpt's stress test system is backend-aware but backend-agnostic. The core orchestrates all workloads; your backend provides the hardware access layer. Implementing custom stress tests is optional but recommended for vendors whose hardware may not have a standard PyTorch device integration.
3.1 How Stress Tests Use Backends
Stress tests subclass StressTest (warpt/stress/base.py). They receive a AcceleratorBackend instance via constructor injection and use it primarily for one thing: getting the correct PyTorch device string via get_pytorch_device_string().
class StressTest(ABC):
@abstractmethod def get_pretty_name(self) -> str
@abstractmethod def get_description(self) -> str
@abstractmethod def get_category(self) -> TestCategory
@abstractmethod def is_available(self) -> bool
@abstractmethod def validate_configuration(self) -> None
@abstractmethod def setup(self) -> None
@abstractmethod def teardown(self) -> None
@abstractmethod def execute_test(self, duration, iterations) -> Any
def run(self, duration, iterations=1) -> Any:
self.validate_configuration()
self.setup()
try:
self.warmup()
return self.execute_test(duration, iterations)
finally:
self.teardown()
3.2 Two Paths for Stress Test Support
Path A: Your hardware has a PyTorch integration. If PyTorch recognizes your device string (e.g., "tt:0" via torch.compile(backend='tt')), then existing warpt stress tests (matrix multiply, memory bandwidth, etc.) work automatically. Your get_pytorch_device_string() just returns the right string.
Path B: Your hardware does not have a PyTorch integration. You write custom stress test classes in warpt/stress/ that exercise your hardware through its native SDK. Same StressTest ABC interface, just different internals in execute_test().
3.3 Test Discovery
The TestRegistry (warpt/stress/registry.py) auto-scans warpt/stress/*.py for non-abstract StressTest subclasses. Any new test file you add is automatically discovered — no registration required.
3.4 Custom Result Models
If you take Path B specified in sub-section 3.2 and write custom stress tests using your native SDK, your execute_test() method can return any object — the return type is Any. However, for structured results that integrate cleanly with warpt's reporting and profiling pipeline, you should define a custom result model.
Follow the same Pydantic BaseModel pattern used by GPUDeviceResult in warpt/models/stress_models.py:
# warpt/models/stress_models.py
class YourVendorStressResult(BaseModel):
device_index: int
test_name: str
duration_seconds: float
operations_per_second: float
peak_power_watts: float | None = None
peak_temperature_celsius: float | None = None
vendor_metrics: dict[str, Any] | None = None # SDK-specific extras
The results collector serializes whatever execute_test() returns, so as long as your model is JSON-serializable (which Pydantic models are by default), it will flow through warpt's reporting layer correctly.
Add your model to warpt/models/stress_models.py and export it from warpt/models/__init__.py.
4. Registration: Wiring Your Backend In
warpt currently uses a hardcoded priority chain for backend discovery. There is no plugin registry or entry-point system. To add your backend, you modify two files: warpt/backends/factory.py (backend registration) and pyproject.toml (package registration).
4.1 Backend Factory
File: warpt/backends/factory.py
The factory tries each vendor backend in priority order. The first backend where is_available() returns True wins. Add a new try/except block for your backend:
def get_gpu_backend() -> AcceleratorBackend:
# Priority 1: NVIDIA (reference implementation)
try:
from warpt.backends.nvidia import NvidiaBackend
backend = NvidiaBackend()
if backend.is_available():
return backend
except Exception:
pass
# ← ADD YOUR BACKEND HERE
# Priority N: YourVendor
try:
from warpt.backends.yourvendor import YourVendorBackend
backend = YourVendorBackend()
if backend.is_available():
return backend
except Exception:
pass
raise RuntimeError("No accelerators detected on this system. ...")
The lazy import pattern (importing inside the try block) is intentional. It means your vendor SDK is only imported if earlier backends fail, so users without your SDK installed won't see ImportErrors.
4.2 Package Registration
Add your backend's package path to the [tool.setuptools] packages list in pyproject.toml so it gets included in builds.
5. Dependencies and Packaging
Currently, nvidia-ml-py is a hard dependency in pyproject.toml. For multi-vendor support, vendor SDKs may be declared as optional extras so that users only install the dependencies they need.
5.1 Current Structure
[project]
dependencies = [
"click>=8.0.0",
"psutil>=5.9.0",
"pydantic>=2.0.0",
"nvidia-ml-py>=12.0.0", # Currently hard dep
]
[project.optional-dependencies]
dev = ["pytest>=7.0.0", "black>=22.0.0", "ruff>=0.1.0"]
stress = ["torch>=2.0.0", "numpy>=1.24.0"]
5.2 Recommended Pattern for New Backends
When submitting your backend, declare your vendor SDK as an optional dependency group:
[project.optional-dependencies]
nvidia = ["nvidia-ml-py>=12.0.0"]
tenstorrent = ["pyluwen>=0.7.11", "tt-tools-common>=1.4.28"]
stress = ["torch>=2.0.0", "numpy>=1.24.0"]
This lets users install only what they need: pip install warpt[tenstorrent]
6. Testing Without Hardware
warpt's test suite uses mock-based testing so that backend tests can run in CI without physical hardware. Follow this pattern for your backend tests.
6.1 Factory Fallthrough Test Pattern
From tests/test_backends_factory.py — the key pattern is using patch.dict(sys.modules, ...) to mock vendor imports and MagicMock() for backend instances:
def test_get_gpu_backend_yourvendor():
"""Mock YourVendorBackend, verify factory returns it."""
# Mock all higher-priority backends as unavailable
mock_nvidia = MagicMock()
mock_nvidia.NvidiaBackend.side_effect = RuntimeError("No NVML")
# Mock your backend as available
mock_yours = MagicMock()
mock_instance = MagicMock()
mock_instance.is_available.return_value = True
mock_yours.YourVendorBackend.return_value = mock_instance
with patch.dict(sys.modules, {
"warpt.backends.nvidia": mock_nvidia,
"warpt.backends.yourvendor": mock_yours,
}):
backend = get_gpu_backend()
assert backend == mock_instance
6.2 Key Test Files
| Test File | What It Tests |
|---|---|
| test_backends_factory.py | Factory priority chain and fallthrough logic |
| test_nvidia_toolkit_detection.py | Mocking CLI tools (shutil.which, subprocess.run) |
| test_stress_registry.py | Test auto-discovery |
| test_stress_results.py | Result collection and emission |
7. Runtime Backend Selection
Currently, warpt uses auto-detection only. There are no CLI flags, config files, or environment variables for selecting a backend. The factory tries backends in priority order and uses the first one where is_available() returns True.
This means your backend will be used automatically on systems where your hardware is present and higher-priority backends are not available. On systems with multiple vendor accelerators, the factory priority order determines which backend wins.
Future improvement: warpt may add explicit backend selection via a
--backendCLI flag orWARPT_BACKENDenvironment variable. If you need to test your backend on a system that also has NVIDIA GPUs, you can temporarily reorder the priority chain infactory.pyduring development.
EarthFrame Inc. | earthframe.com | Questions? Open an issue on the warpt repository.