Skip to content

Instantly share code, notes, and snippets.

@syldrathecat
Last active May 4, 2024 01:08
Show Gist options
  • Save syldrathecat/36318eb1368f676c0deaba25c583a0c1 to your computer and use it in GitHub Desktop.
Save syldrathecat/36318eb1368f676c0deaba25c583a0c1 to your computer and use it in GitHub Desktop.
// 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