Skip to content

Add Cray/HPE pm_counters backend with cross-vendor GPU correlation hints - #9

Draft
vancraar wants to merge 7 commits into
developfrom
feature/cray-pm-counters
Draft

Add Cray/HPE pm_counters backend with cross-vendor GPU correlation hints#9
vancraar wants to merge 7 commits into
developfrom
feature/cray-pm-counters

Conversation

@vancraar

Copy link
Copy Markdown
Member

Cray/HPE pm_counters backend

New cray_pm_counters backend reading whole-node and per-accelerator
power/energy from Cray/HPE's /sys/cray/pm_counters sysfs interface, to
validate hws's software-side energy measurements against measured
hardware ground truth on HLRS's Hunter system (HPE Cray EX, AMD MI300A
APU nodes). The counter set is discovered and classified at runtime
instead of hardcoded, since the exposed files differ across PM counter
versions and node generations. Confirmed against a real Hunter mi300a
node (23 files, PM counters v3). Includes CTest-based regression tests
using fixtures from that real dump.

Cross-vendor accel[i] <-> GPU correlation hints

pm_counters exposes per-accelerator counters as accel[i]_energy/
accel[i]_power, but doesn't say which physical GPU (as seen by
HIP/CUDA) each index corresponds to. Adds a best-effort correlation
guess, generalized from AMD-only to also cover NVIDIA (pm_counters uses
the same accel[i] naming convention on Grace-Hopper/EX254n nodes):
gpu_amd_hardware_sampler/gpu_nvidia_hardware_sampler each gained a
pci_bus_id() querying the same handle already used for their actual
measurements, and a shared, vendor-agnostic
accel_correlation_yaml_block() builds the resulting YAML hint block
that system_hardware_sampler appends to dump_yaml/dump_yaml_global.

Python bindings for cray_pm_counters

Mirrors the existing per-backend binding pattern (one .cpp file,
HWS_FOR_CRAY_PM_COUNTERS_ENABLED-gated). Built and tested locally and
on a real Hunter mi300a node via a PBS job, including a dump_yaml
roundtrip with the correlation-hints section.

Known limitations

Also included

A pre-existing, unrelated fix already merged onto this branch: turbostat
subprocess deadlock and unreliable short sampling intervals in the CPU
backend (commit 1884d03).

Reads whole-node power/energy from Cray/HPE's /sys/cray/pm_counters
sysfs interface, to validate hws's software energy measurements
against measured hardware ground truth on HLRS's Hunter system.
The counter set is discovered and classified at runtime instead of
hardcoded, since the exposed files differ across PM counter versions
and node generations; confirmed against a real Hunter mi300a node
(23 files, PM counters v3). Includes CTest-based regression tests
using fixtures from that real dump.
Generalizes the pm_counters accel[i] index guess from AMD-only to also
cover NVIDIA, since pm_counters exposes the same accel[i] naming
convention on Grace-Hopper (EX254n) nodes. Extracts the vendor-specific
YAML formatting into a shared, vendor-agnostic
hws::detail::accel_correlation_yaml_block() so AMD and NVIDIA share one
code path in system_hardware_sampler's device_correlation_hints.

Both gpu_amd_hardware_sampler and gpu_nvidia_hardware_sampler gained a
pci_bus_id() that queries the same API/handle each sampler already uses
for its actual measurements (ROCm SMI's rsmi_dev_pci_id_get(device_id_),
NVML's nvmlDeviceGetPciInfo_v3() on the already-resolved handle) instead
of a different API family (HIP/CUDA's own PCI bus ID calls). Those differ
from ROCm SMI's/NVML's device enumeration under
HIP_VISIBLE_DEVICES/CUDA_VISIBLE_DEVICES, which could otherwise silently
attribute a correlation guess to the wrong physical device.
dump_yaml()/dump_yaml_global() wrote each sampler's own YAML document
directly instead of going through as_yaml_string(), so
device_correlation_hints_as_yaml_string() never made it into a dumped
YAML file even though as_yaml_string() included it correctly. Found by
running the actual example program on a real Hunter mi300a node - the
correlation section was silently missing from track.yaml.

dump_yaml(std::string)/dump_yaml(path) now forward to dump_yaml(const
char*) instead of duplicating the sampler loop.
…vals

The CPU turbostat backend could deadlock: run_subprocess() never combined
the child's stdout/stderr, so an unread stderr pipe could fill up and block
the child forever, wedging subprocess_join() (reproduced via gdb/wchan on
two architectures). Fixed by passing subprocess_option_combined_stdout_stderr.

That fix has a side effect: turbostat's own diagnostic messages (e.g.
"Disabling Low Power Idle CPU output") now land in the same stream hws
parses, which used to assume the header/value pair were always the first
two lines. Parsing now takes the *last* two lines instead, since turbostat
always prints diagnostics before its one-shot table, never after.

Also fixed: turbostat's -i interval was hardcoded to 0.001s (1ms), which
empirically (four real machines, full-core load) causes physically
implausible readings (multi-kW power, multi-GHz clocks) because turbostat's
own internal timing calculation degrades under load at short intervals.
This is now the configurable HWS_TURBOSTAT_INTERVAL CMake option; the
default stays at 0.001 for backwards compatibility with existing sudoers
allowlists and paper-reproducibility setups, but a longer interval (e.g. 1s)
is documented as the reliable choice. Related changes:
- warn at runtime if sampling_interval is shorter than HWS_TURBOSTAT_INTERVAL
- sleep_until() instead of sleep_for() so a slow turbostat call doesn't also
  get a full extra sampling_interval tacked on afterward
- only invoke turbostat if an enabled sample_category actually needs it
gpu_amd_hardware_sampler used the HIP-relative device index directly as
the ROCm SMI index for every rsmi_dev_* call. ROCm SMI's own device
enumeration isn't filtered by HIP_VISIBLE_DEVICES/ROCR_VISIBLE_DEVICES the
way HIP's is, so under a non-default visibility mask the sampler measured
the wrong physical GPU.

Fix: resolve the actual ROCm SMI index once at construction via a PCI-bus-ID
match against every ROCm SMI device, throwing if no match is found instead
of silently falling back to the HIP-relative index. The original HIP-relative
index is kept in a new hip_device_id_ member for the one HIP call that still
needs HIP-space, and for system_hardware_sampler's device_correlation_hints
output.

Also adds the missing find_dependency(MPI) to hwsConfig.cmake.in, alongside
the existing ones for CUDA/HIP/ROCm-SMI/Level-Zero.
Mirrors the existing per-backend binding pattern: one .cpp file
(bindings/cray_pm_counters_hardware_sampler.cpp) binding
CrayPmCountersGeneralSamples, CrayPmCountersPowerSamples and
CrayPmCountersHardwareSampler (constructors, general_samples,
power_samples, discovered_accel_indices, samples_only_as_yaml_string,
__repr__), gated behind HWS_FOR_CRAY_PM_COUNTERS_ENABLED like every other
backend's bindings. device_identification() stays unbound, consistent
with every other backend.

Built and tested locally (non-MPI build) and on a real Hunter mi300a
node via a PBS job: SystemHardwareSampler auto-discovery, direct
CrayPmCountersHardwareSampler construction, and a dump_yaml roundtrip
including the device_correlation_hints section all work correctly.
@vancraar
vancraar requested a review from TimThuering August 13, 2026 14:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant