Last active
May 4, 2024 01:08
-
-
Save syldrathecat/36318eb1368f676c0deaba25c583a0c1 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// build: gcc amdgpustats.c -o amdgpustats `pkg-config --cflags --libs libdrm` | |
// Requires Linux 4.12 or later, and libdrm 2.4.77 or later | |
#include <assert.h> | |
#include <errno.h> | |
#include <limits.h> | |
#include <stdint.h> | |
#include <stdio.h> | |
#include <string.h> | |
#include <stdlib.h> | |
#include <time.h> | |
#include <alloca.h> | |
#include <sys/types.h> | |
#include <sys/stat.h> | |
#include <fcntl.h> | |
#include <dirent.h> | |
#include <unistd.h> | |
#include <xf86drm.h> | |
#include <amdgpu_drm.h> | |
#ifndef AMDGPU_INFO_SENSOR | |
#error libdrm version is not new enough | |
#endif // AMDGPU_INFO_SENSOR | |
// Polling rate for GPU load reporting in microseconds | |
#define POLL_INTERVAL_US (1000000 / 100) | |
// DRI device location | |
#define DEV_DRI_PATH "/dev/dri/" | |
#define DEV_DRI_PATH_LEN ((sizeof DEV_DRI_PATH) - 1) | |
// Attempts to open the n-th video card that the amdgpu drm driver is managing | |
// e.g wanted_card=0 would find the first amdgpu card | |
// *drm_fd is set to the matching device's file descriptor | |
// the caller must call close() on *drm_fd | |
// Returns 0 if the search did not fail | |
int open_video_card(int wanted_card, int* drm_fd) | |
{ | |
int card_count = 0; | |
DIR* dri_dir = opendir(DEV_DRI_PATH); | |
if (!dri_dir) | |
{ | |
perror("Could not open " DEV_DRI_PATH); | |
return 1; | |
} | |
struct dirent* dir_entry; | |
while ((dir_entry = readdir(dri_dir))) | |
{ | |
size_t name_length = strlen(dir_entry->d_name); | |
// skip the . and .. directory entries | |
if (strcmp(dir_entry->d_name, ".") == 0 || strcmp(dir_entry->d_name, "..") == 0) | |
continue; | |
size_t path_length = name_length + DEV_DRI_PATH_LEN; | |
assert(path_length > name_length); | |
// construct the device name | |
char* devnamebuf = malloc(path_length + 1); | |
memcpy(devnamebuf, DEV_DRI_PATH, DEV_DRI_PATH_LEN); | |
memcpy(devnamebuf + DEV_DRI_PATH_LEN, dir_entry->d_name, name_length); | |
devnamebuf[path_length] = '\0'; | |
int check_drm_fd = open(devnamebuf, 0); | |
if (check_drm_fd != 0) | |
{ | |
// Check the DRM driver version of the device | |
drmVersionPtr version = drmGetVersion(check_drm_fd); | |
if (!version) | |
continue; | |
assert(version->name); | |
int is_amdgpu = (strcmp(version->name, "amdgpu") == 0); | |
drmFreeVersion(version); | |
if (is_amdgpu) | |
{ | |
if (card_count++ == wanted_card) | |
{ | |
if (drm_fd) | |
*drm_fd = check_drm_fd; | |
free(devnamebuf); | |
break; | |
} | |
} | |
} | |
free(devnamebuf); | |
} | |
closedir(dri_dir); | |
return 0; | |
} | |
// Maximum SCLK and MCLK values in Hz | |
int read_gpu_max_clock(int drm_fd, uint64_t* max_sclk, uint64_t* max_mclk) | |
{ | |
struct drm_amdgpu_info_device device; | |
memset(&device, 0, sizeof device); | |
struct drm_amdgpu_info request; | |
memset(&request, 0, sizeof request); | |
request.return_pointer = (uint64_t)&device; | |
request.return_size = sizeof device; | |
request.query = AMDGPU_INFO_DEV_INFO; | |
int result = drmCommandWrite(drm_fd, DRM_AMDGPU_INFO, &request, sizeof request); | |
if (result == 0) | |
{ | |
*max_sclk = device.max_engine_clock; | |
*max_mclk = device.max_memory_clock; | |
} | |
return result; | |
} | |
// Maximum VRAM usage in bytes | |
int read_gpu_max_vram(int drm_fd, uint64_t* max_vram) | |
{ | |
struct drm_amdgpu_info_vram_gtt vram_gtt; | |
memset(&vram_gtt, 0, sizeof vram_gtt); | |
struct drm_amdgpu_info request; | |
memset(&request, 0, sizeof request); | |
request.return_pointer = (uint64_t)&vram_gtt; | |
request.return_size = sizeof vram_gtt; | |
request.query = AMDGPU_INFO_VRAM_GTT; | |
int result = drmCommandWrite(drm_fd, DRM_AMDGPU_INFO, &request, sizeof request); | |
if (result == 0) | |
*max_vram = vram_gtt.vram_size; | |
return result; | |
} | |
// Current VRAM usage in bytes | |
int read_gpu_used_vram(int drm_fd, uint64_t* used_vram) | |
{ | |
struct drm_amdgpu_info request; | |
memset(&request, 0, sizeof request); | |
request.return_pointer = (uint64_t)used_vram; | |
request.return_size = sizeof *used_vram; | |
request.query = AMDGPU_INFO_VRAM_USAGE; | |
return drmCommandWrite(drm_fd, DRM_AMDGPU_INFO, &request, sizeof request); | |
} | |
// Common function for reading GPU sensor registers | |
int read_gpu_reg(int drm_fd, int reg, uint32_t* value) | |
{ | |
struct drm_amdgpu_info request; | |
memset(&request, 0, sizeof request); | |
request.return_pointer = (uint64_t)value; | |
request.return_size = sizeof *value; | |
request.query = AMDGPU_INFO_SENSOR; | |
request.sensor_info.type = reg; | |
return drmCommandWrite(drm_fd, DRM_AMDGPU_INFO, &request, sizeof request); | |
} | |
// Shader core clock speed in MHz | |
int read_gpu_sclk(int drm_fd, uint32_t* value) | |
{ | |
return read_gpu_reg(drm_fd, AMDGPU_INFO_SENSOR_GFX_SCLK, value); | |
} | |
// Memory clock speed in MHz | |
int read_gpu_mclk(int drm_fd, uint32_t* value) | |
{ | |
return read_gpu_reg(drm_fd, AMDGPU_INFO_SENSOR_GFX_MCLK, value); | |
} | |
// GPU temperature in millidegrees C | |
int read_gpu_temp(int drm_fd, uint32_t* value) | |
{ | |
return read_gpu_reg(drm_fd, AMDGPU_INFO_SENSOR_GPU_TEMP, value); | |
} | |
// GPU load measurement between 0 and 100 | |
// Fluctuates like crazy and needs to be polled rapidly for an accurate reading | |
int read_gpu_load(int drm_fd, uint32_t* value) | |
{ | |
return read_gpu_reg(drm_fd, AMDGPU_INFO_SENSOR_GPU_LOAD, value); | |
} | |
// GPU power draw in watts (W) | |
int read_gpu_power(int drm_fd, uint32_t* value) | |
{ | |
return read_gpu_reg(drm_fd, AMDGPU_INFO_SENSOR_GPU_AVG_POWER, value); | |
} | |
// GPU core voltage in millivolts (mV) | |
int read_gpu_voltage(int drm_fd, uint32_t* value) | |
{ | |
return read_gpu_reg(drm_fd, AMDGPU_INFO_SENSOR_VDDGFX, value); | |
} | |
// Reads the first line of text from a read-only file | |
void read_line(const char* path, char* buffer, size_t buffer_size) | |
{ | |
int fd = open(path, O_RDONLY); | |
if (fd == 0) | |
{ | |
buffer[0] = '\0'; | |
return; | |
} | |
int pos = 0; | |
int result = 0; | |
while ((result = read(fd, buffer + pos, (buffer_size - 1) - pos)) > 0) | |
pos += result; | |
buffer[pos--] = '\0'; | |
while (pos >= 0 && buffer[pos] == '\n') | |
buffer[pos--] = '\0'; | |
close(fd); | |
} | |
// Reads the first line of text from a read-only file as a number | |
long read_value(const char* path) | |
{ | |
char buffer[32]; | |
read_line(path, buffer, sizeof buffer); | |
return atol(buffer); | |
} | |
// Attempts to determine the path to the first amdgpu hwmon entry in a directory | |
// Mutates its argument in-place! | |
// Returns 0 if the search did not fail | |
int locate_hwmon(char* path) | |
{ | |
int found = 0; | |
DIR* hwmon_dir = opendir(path); | |
if (!hwmon_dir) | |
{ | |
fprintf(stderr, "Could not open %s: %s\n", path, strerror(errno)); | |
return 1; | |
} | |
size_t path_length = strlen(path); | |
struct dirent* dir_entry; | |
while ((dir_entry = readdir(hwmon_dir))) | |
{ | |
size_t name_length = strlen(dir_entry->d_name); | |
// skip the . and .. directory entries | |
if (strcmp(dir_entry->d_name, ".") == 0 || strcmp(dir_entry->d_name, "..") == 0) | |
continue; | |
size_t d_name_length = strlen(dir_entry->d_name); | |
assert(path_length + d_name_length + 5 < PATH_MAX); | |
// construct the device name | |
memcpy(path + path_length, dir_entry->d_name, d_name_length); | |
memcpy(path + path_length + d_name_length, "/name", 5); | |
path[path_length + d_name_length + 5] = '\0'; | |
char driver_name[32]; | |
read_line(path, driver_name, 32); | |
int is_amdgpu = (strcmp(driver_name, "amdgpu") == 0); | |
if (is_amdgpu) | |
{ | |
// trim "name" from the end of the path | |
path[path_length + d_name_length + 1] = '\0'; | |
closedir(hwmon_dir); | |
return 0; | |
} | |
path[path_length] = '\0'; | |
} | |
closedir(hwmon_dir); | |
return 1; | |
} | |
int main() | |
{ | |
// amdgpu driver assumption | |
assert(sizeof(uint32_t*) <= sizeof(uint64_t)); | |
int wanted_card = 0; | |
int drm_fd = 0; | |
int hwmon_available = 0; | |
char hwmon_path[PATH_MAX]; | |
char hwmon_power1_average_path[PATH_MAX]; | |
char hwmon_power1_cap_path[PATH_MAX]; | |
if (open_video_card(wanted_card, &drm_fd) != 0) | |
return 1; | |
if (drm_fd == 0) | |
{ | |
fprintf(stderr, "amdgpu card #%d not found\n", wanted_card); | |
return 1; | |
} | |
sprintf(hwmon_path, "/sys/class/drm/card%d/device/hwmon/", wanted_card); | |
// mutates hwmon_path in-place! | |
hwmon_available = !locate_hwmon(hwmon_path); | |
if (hwmon_available) | |
{ | |
int result = snprintf(hwmon_power1_average_path, PATH_MAX, "%s%s", hwmon_path, "power1_average"); | |
if (result == PATH_MAX) | |
hwmon_available = 0; | |
result = snprintf(hwmon_power1_cap_path, PATH_MAX - 1, "%s%s", hwmon_path, "power1_cap"); | |
if (result == PATH_MAX) | |
hwmon_available = 0; | |
} | |
uint32_t load_raw = 0; | |
uint64_t used_vram_raw = 0; | |
uint32_t power_raw = 0; | |
uint32_t sclk_raw = 0; | |
uint32_t mclk_raw = 0; | |
uint32_t temp_raw = 0; | |
uint32_t voltage_raw = 0; | |
uint64_t max_vram_raw = 0; | |
uint64_t max_sclk_raw = 0; | |
uint64_t max_mclk_raw = 0; | |
if (read_gpu_max_vram(drm_fd, &max_vram_raw) != 0) | |
perror("Failed to read maximum VRAM"); | |
if (read_gpu_max_clock(drm_fd, &max_sclk_raw, &max_mclk_raw) != 0) | |
perror("Failed to read maximum clock speeds"); | |
time_t then = time(NULL); | |
int max_vram_mb = (int)(max_vram_raw / 1024 / 1024); | |
int max_sclk_mhz = (int)(max_sclk_raw / 1000); | |
int max_mclk_mhz = (int)(max_mclk_raw / 1000); | |
uint32_t max_sclk_div = (uint32_t)max_sclk_mhz; | |
uint32_t load_acc = 0; | |
uint32_t adj_load_acc = 0; | |
uint32_t load_samples = 0; | |
long precise_power_mw = 0; | |
long precise_power_cap_mw = 0; | |
while (1) | |
{ | |
read_gpu_load(drm_fd, &load_raw); | |
read_gpu_sclk(drm_fd, &sclk_raw); | |
load_acc += load_raw; | |
adj_load_acc += (max_sclk_div == 0) ? 0 : load_raw * sclk_raw / max_sclk_div; | |
++load_samples; | |
time_t now = time(NULL); | |
if (then != now) | |
{ | |
then = now; | |
read_gpu_used_vram(drm_fd, &used_vram_raw); | |
read_gpu_power(drm_fd, &power_raw); | |
read_gpu_mclk(drm_fd, &mclk_raw); | |
read_gpu_temp(drm_fd, &temp_raw); | |
read_gpu_voltage(drm_fd, &voltage_raw); | |
// hwmon provides a much more precise power draw reading | |
if (hwmon_available) | |
{ | |
precise_power_mw = read_value(hwmon_power1_average_path) / 1000; | |
precise_power_cap_mw = read_value(hwmon_power1_cap_path) / 1000; | |
} | |
int load_avg = (load_samples == 0) ? 0 : load_acc / load_samples; | |
int adj_load_avg = (load_samples == 0) ? 0 : adj_load_acc / load_samples; | |
int used_vram_mb = (int)(used_vram_raw / 1024 / 1024); | |
int power_w = (int)(power_raw); | |
int sclk_mhz = (int)(sclk_raw); | |
int mclk_mhz = (int)(mclk_raw); | |
int temp_c = (int)(temp_raw / 1000); | |
int voltage_mv = (int)(voltage_raw); | |
printf(" GPU Load: %d%% (clock adj.: %d%%)\n", load_avg, adj_load_avg); | |
printf(" VRAM Usage: %d / %d MB\n", used_vram_mb, max_vram_mb); | |
// Max MHz values appear to be wrong if overclocked using the new pp_od_clk_voltage interface | |
printf("Shader Clock: %d / %d MHz\n", sclk_mhz, max_sclk_mhz); | |
printf("Memory Clock: %d / %d MHz\n", mclk_mhz, max_mclk_mhz); | |
printf(" Temperature: %d °C\n", temp_c); | |
if (precise_power_mw > 0 && precise_power_cap_mw > 0) | |
{ | |
printf(" Power Draw: %ld.%03ld / %ld.%03ld W\n", | |
precise_power_mw / 1000, | |
precise_power_mw % 1000, | |
precise_power_cap_mw / 1000, | |
precise_power_cap_mw % 1000 | |
); | |
} | |
else | |
{ | |
printf(" Power Draw: %d W\n", power_w); | |
} | |
printf(" VDDGFX: %d mV\n", voltage_mv); | |
puts(""); | |
load_acc = 0; | |
adj_load_acc = 0; | |
load_samples = 0; | |
} | |
usleep(POLL_INTERVAL_US); | |
} | |
close(drm_fd); | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment