12 Commits

Author SHA1 Message Date
9cab5fb0c3 Add PKGBUILD 2026-09-10 18:57:00 -04:00
Ole Algoritme
b5e11b1d75 Merge pull request #50 from wallentx/agent/rtx-a4000-24b0
Add RTX A4000 support
2026-08-01 00:03:17 +02:00
Ole Algoritme
8ae6f2199f Merge pull request #53 from wallentx/agent/preserve-64-bit-bar0
Preserve full-width BAR0 addresses
2026-08-01 00:03:02 +02:00
Ole Algoritme
147f79f871 Merge pull request #52 from wallentx/agent/correct-a6000-device-entries
Correct RTX 6000 Ada device name
2026-08-01 00:02:49 +02:00
wallentx
0912afa296 Correct RTX 6000 Ada device name 2026-07-23 00:03:19 -05:00
wallentx
d83355b2cc Preserve full-width BAR0 addresses 2026-07-22 23:43:30 -05:00
wallentx
2c179c395b Add RTX A4000 support 2026-07-22 23:41:22 -05:00
Ole Algoritme
7ef74c5997 Merge pull request #43 from ElXreno/add-rtx-4060-mobile
Add RTX 4060 Max-Q / Mobile (AD107, 0x28e0) support
2026-07-19 10:48:47 +02:00
ElXreno
caf23c2212 Add RTX 4060 Max-Q / Mobile (AD107, 0x28e0) support 2026-07-19 11:39:34 +03:00
Ole Algoritme
0ace205dfd README: link to gpuwatch for a Blackwell TUI/standalone app 2026-07-16 00:58:26 +02:00
Ole Algoritme
8dfc9a5e29 Merge pull request #42 from olealgoritme/add-rtx5090-gddr7
Add experimental RTX 5090 (GDDR7) support
2026-07-15 15:44:10 +02:00
Ole Algoritme
9259d9a2d4 Add experimental RTX 5090 (GDDR7) memory temperature support
Reads the per-module GDDR7 DRAM sensors (FBPA DQR) directly and reports
the hotspot by default; --per-module lists each of the 8 modules.
Adds a per-device decode so the existing GDDR6/6X path is unchanged.
2026-07-15 15:42:23 +02:00
6 changed files with 199 additions and 16 deletions

8
.gitignore vendored
View File

@@ -2,3 +2,11 @@ gddr6
build/ build/
*.o *.o
*.a *.a
# local reverse-engineering probes / scratch tools
probe_*
probe5090*
scan_therm*
measure_gentle*
memload*
!*.md

29
PKGBUILD Normal file
View File

@@ -0,0 +1,29 @@
pkgname=gddr6-git
pkgver=r105.b5e11b1
pkgrel=1
pkgdesc="Linux based GDDR6/GDDR6X VRAM temperature reader for NVIDIA RTX 3000/4000 series GPUs"
arch=('x86_64')
url="https://github.com/olealgoritme/gddr6"
license=('GPL3')
depends=('pciutils')
makedepends=('git' 'gcc' 'cmake' 'make')
provides=('gddr6')
conflicts=('gddr6')
source=('gddr6-git::git+https://github.com/olealgoritme/gddr6')
md5sums=('SKIP')
pkgver() {
cd "$srcdir/gddr6-git"
printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
}
build() {
cd "$srcdir/gddr6-git"
cmake -B build -DCMAKE_INSTALL_PREFIX=/usr
cmake --build build
}
package() {
cd "$srcdir/gddr6-git"
DESTDIR="$pkgdir" cmake --install build
}

View File

@@ -3,6 +3,22 @@
Reads GDDR6/GDDR6X VRAM memory temperatures from multiple supported NVIDIA GPUs found in a host Linux system. Reads GDDR6/GDDR6X VRAM memory temperatures from multiple supported NVIDIA GPUs found in a host Linux system.
These findings are based on reverse engineering of the NVIDIA GPU Linux driver. These findings are based on reverse engineering of the NVIDIA GPU Linux driver.
### Experimental: RTX 5090 (GDDR7)
Experimental support for the RTX 5090 (Blackwell / GB202, GDDR7) is included. It reads the per-module
DRAM sensors directly and reports the hotspot (hottest module) by default:
```
sudo gddr6 # VRAM hotspot temperature
sudo gddr6 --per-module # each of the 8 GDDR7 modules separately
```
This is reverse-engineered and unofficial (NVIDIA does not expose memory temperature via nvidia-smi/NVML
on this card) — treat the readings as approximate.
For a dedicated Blackwell app with a TUI (plus ASCII/JSON output) and a reusable C library, see
[**gpuwatch**](https://github.com/olealgoritme/gpuwatch).
## Prerequisites ## Prerequisites
@@ -46,6 +62,7 @@ sudo gddr6
``` ```
## Supported GPUs ## Supported GPUs
- RTX 5090 (GB202) — GDDR7, experimental (see above)
- RTX 4090 (AD102) - RTX 4090 (AD102)
- RTX 4080 Super (AD103) - RTX 4080 Super (AD103)
- RTX 4080 (AD103) - RTX 4080 (AD103)
@@ -53,6 +70,7 @@ sudo gddr6
- RTX 4070 Ti (AD104) - RTX 4070 Ti (AD104)
- RTX 4070 Super (AD104) - RTX 4070 Super (AD104)
- RTX 4070 (AD104) - RTX 4070 (AD104)
- RTX 4060 Max-Q / Mobile (AD107)
- RTX 3090 Ti (GA102) - RTX 3090 Ti (GA102)
- RTX 3090 (GA102) - RTX 3090 (GA102)
- RTX 3080 Ti (GA102) - RTX 3080 Ti (GA102)
@@ -61,9 +79,10 @@ sudo gddr6
- RTX 3070 (GA104) - RTX 3070 (GA104)
- RTX 3070 LHR (GA104) - RTX 3070 LHR (GA104)
- RTX A2000 (GA106) - RTX A2000 (GA106)
- RTX A4000 (GA104)
- RTX A4500 (GA102) - RTX A4500 (GA102)
- RTX A5000 (GA102) - RTX A5000 (GA102)
- RTX A6000 (AD102) - RTX 6000 Ada Generation (AD102)
- L4 (AD104) - L4 (AD104)
- L40S (AD102) - L40S (AD102)
- A10 (GA102) - A10 (GA102)

View File

@@ -2,6 +2,7 @@
#include "gddr6.h" #include "gddr6.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h>
#include <signal.h> #include <signal.h>
void register_signal_handlers(void) void register_signal_handlers(void)
@@ -18,6 +19,21 @@ void register_signal_handlers(void)
int main(int argc, char **argv) int main(int argc, char **argv)
{ {
int per_module = 0;
for (int i = 1; i < argc; i++)
{
if (strcmp(argv[i], "--per-module") == 0)
per_module = 1;
else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0)
{
printf("Usage: %s [--per-module]\n"
" (default) show VRAM temperature (Blackwell: hottest module)\n"
" --per-module show each GDDR7 module separately (Blackwell only)\n",
argv[0]);
return 0;
}
}
register_signal_handlers(); register_signal_handlers();
gddr6_init(); gddr6_init();
int num_devs = gddr6_detect_compatible_gpus(); int num_devs = gddr6_detect_compatible_gpus();
@@ -29,7 +45,7 @@ int main(int argc, char **argv)
} }
gddr6_memory_map(); gddr6_memory_map();
gddr6_monitor_temperatures(); gddr6_monitor_temperatures(per_module);
return 0; return 0;
} }

View File

@@ -4,18 +4,26 @@
#include <stdint.h> #include <stdint.h>
// How to convert a raw register read into degrees Celsius.
enum temp_decode {
DECODE_ADA = 0, // Ada/Ampere: (raw & 0xfff) / 32
DECODE_GDDR_MRCODE, // Blackwell FBPA DQR: byte in bits 23:16 is a GDDR temp
// MR-code; C = (code-20)*2 for code>19, else -(40-code*2)
};
struct device struct device
{ {
uint32_t bar0; uint64_t bar0;
uint8_t bus, dev, func; uint8_t bus, dev, func;
uint32_t offset; uint32_t offset;
enum temp_decode decode; // how to turn the raw reg into Celsius
uint16_t dev_id; uint16_t dev_id;
const char *vram; const char *vram;
const char *arch; const char *arch;
const char *name; const char *name;
void *mapped_addr; void *mapped_addr;
uint32_t phys_addr; uint64_t phys_addr;
uint32_t base_offset; uint64_t base_offset;
}; };
struct gddr6_ctx { struct gddr6_ctx {
@@ -27,7 +35,7 @@ struct gddr6_ctx {
void gddr6_init(void); void gddr6_init(void);
void gddr6_memory_map(void); void gddr6_memory_map(void);
void gddr6_cleanup(int signal); void gddr6_cleanup(int signal);
void gddr6_monitor_temperatures(void); void gddr6_monitor_temperatures(int per_module);
int gddr6_detect_compatible_gpus(void); int gddr6_detect_compatible_gpus(void);
#endif // GDDR6_H #endif // GDDR6_H

View File

@@ -1,4 +1,5 @@
// gddr6.c // gddr6.c
#define _FILE_OFFSET_BITS 64
#define _GNU_SOURCE #define _GNU_SOURCE
#include "gddr6.h" #include "gddr6.h"
@@ -11,7 +12,6 @@
#include <fcntl.h> #include <fcntl.h>
#include <sys/mman.h> #include <sys/mman.h>
#include <pci/pci.h> #include <pci/pci.h>
#include <signal.h>
#define PG_SZ sysconf(_SC_PAGE_SIZE) #define PG_SZ sysconf(_SC_PAGE_SIZE)
#define PRINT_ERROR() \ #define PRINT_ERROR() \
@@ -23,6 +23,7 @@
#define MAX_DEVICES 32 #define MAX_DEVICES 32
struct gddr6_ctx ctx = {0}; struct gddr6_ctx ctx = {0};
// Ada/Ampere GPUs: temperature field is bits [11:0], Celsius = field / 32.
struct device dev_table[] = struct device dev_table[] =
{ {
{ .offset = 0x0000E2A8, .dev_id = 0x2684, .vram = "GDDR6X", .arch = "AD102", .name = "RTX 4090" }, { .offset = 0x0000E2A8, .dev_id = 0x2684, .vram = "GDDR6X", .arch = "AD102", .name = "RTX 4090" },
@@ -34,6 +35,7 @@ struct device dev_table[] =
{ .offset = 0x0000E2A8, .dev_id = 0x2783, .vram = "GDDR6X", .arch = "AD104", .name = "RTX 4070 Super" }, { .offset = 0x0000E2A8, .dev_id = 0x2783, .vram = "GDDR6X", .arch = "AD104", .name = "RTX 4070 Super" },
{ .offset = 0x0000E2A8, .dev_id = 0x2786, .vram = "GDDR6X", .arch = "AD104", .name = "RTX 4070" }, { .offset = 0x0000E2A8, .dev_id = 0x2786, .vram = "GDDR6X", .arch = "AD104", .name = "RTX 4070" },
{ .offset = 0x0000E2A8, .dev_id = 0x2860, .vram = "GDDR6", .arch = "AD106", .name = "RTX 4070 Max-Q / Mobile" }, { .offset = 0x0000E2A8, .dev_id = 0x2860, .vram = "GDDR6", .arch = "AD106", .name = "RTX 4070 Max-Q / Mobile" },
{ .offset = 0x0000E2A8, .dev_id = 0x28e0, .vram = "GDDR6", .arch = "AD107", .name = "RTX 4060 Max-Q / Mobile" },
{ .offset = 0x0000E2A8, .dev_id = 0x2203, .vram = "GDDR6X", .arch = "GA102", .name = "RTX 3090 Ti" }, { .offset = 0x0000E2A8, .dev_id = 0x2203, .vram = "GDDR6X", .arch = "GA102", .name = "RTX 3090 Ti" },
{ .offset = 0x0000E2A8, .dev_id = 0x2204, .vram = "GDDR6X", .arch = "GA102", .name = "RTX 3090" }, { .offset = 0x0000E2A8, .dev_id = 0x2204, .vram = "GDDR6X", .arch = "GA102", .name = "RTX 3090" },
{ .offset = 0x0000E2A8, .dev_id = 0x2208, .vram = "GDDR6X", .arch = "GA102", .name = "RTX 3080 Ti" }, { .offset = 0x0000E2A8, .dev_id = 0x2208, .vram = "GDDR6X", .arch = "GA102", .name = "RTX 3080 Ti" },
@@ -43,12 +45,21 @@ struct device dev_table[] =
{ .offset = 0x0000EE50, .dev_id = 0x2488, .vram = "GDDR6", .arch = "GA104", .name = "RTX 3070 LHR" }, { .offset = 0x0000EE50, .dev_id = 0x2488, .vram = "GDDR6", .arch = "GA104", .name = "RTX 3070 LHR" },
{ .offset = 0x0000E2A8, .dev_id = 0x2531, .vram = "GDDR6", .arch = "GA106", .name = "RTX A2000" }, { .offset = 0x0000E2A8, .dev_id = 0x2531, .vram = "GDDR6", .arch = "GA106", .name = "RTX A2000" },
{ .offset = 0x0000E2A8, .dev_id = 0x2571, .vram = "GDDR6", .arch = "GA106", .name = "RTX A2000" }, { .offset = 0x0000E2A8, .dev_id = 0x2571, .vram = "GDDR6", .arch = "GA106", .name = "RTX A2000" },
{ .offset = 0x0000E2A8, .dev_id = 0x24b0, .vram = "GDDR6", .arch = "GA104", .name = "RTX A4000" },
{ .offset = 0x0000E2A8, .dev_id = 0x2232, .vram = "GDDR6", .arch = "GA102", .name = "RTX A4500" }, { .offset = 0x0000E2A8, .dev_id = 0x2232, .vram = "GDDR6", .arch = "GA102", .name = "RTX A4500" },
{ .offset = 0x0000E2A8, .dev_id = 0x2231, .vram = "GDDR6", .arch = "GA102", .name = "RTX A5000" }, { .offset = 0x0000E2A8, .dev_id = 0x2231, .vram = "GDDR6", .arch = "GA102", .name = "RTX A5000" },
{ .offset = 0x0000E2A8, .dev_id = 0x26B1, .vram = "GDDR6", .arch = "AD102", .name = "RTX A6000" }, { .offset = 0x0000E2A8, .dev_id = 0x26B1, .vram = "GDDR6", .arch = "AD102", .name = "RTX 6000 Ada Generation" },
{ .offset = 0x0000E2A8, .dev_id = 0x27b8, .vram = "GDDR6", .arch = "AD104", .name = "L4" }, { .offset = 0x0000E2A8, .dev_id = 0x27b8, .vram = "GDDR6", .arch = "AD104", .name = "L4" },
{ .offset = 0x0000E2A8, .dev_id = 0x26b9, .vram = "GDDR6", .arch = "AD102", .name = "L40S" }, { .offset = 0x0000E2A8, .dev_id = 0x26b9, .vram = "GDDR6", .arch = "AD102", .name = "L40S" },
{ .offset = 0x0000E2A8, .dev_id = 0x2236, .vram = "GDDR6", .arch = "GA102", .name = "A10" }, { .offset = 0x0000E2A8, .dev_id = 0x2236, .vram = "GDDR6", .arch = "GA102", .name = "A10" },
// Blackwell GDDR7 memory temperature: the raw FBPA DRAM sensor at
// NV_PFB_FBPA_DQR_STATUS_DQ_IC0_SUBP0 (0x9A24C0) - the register the FBFALCON
// firmware reads. NOT PLM-locked; reads valid data from userspace (validity
// bit 24 of 0x9A24D0). The value is a per-device GDDR temp MR-code in bits
// 23:16; DECODE_GDDR_MRCODE converts it to Celsius. (The documented mem-temp
// reg 0x9A44B0 is PLM-locked and its 0xE2A8 scratch mirror is unpopulated on
// this card, so we read the raw sensor directly.)
{ .offset = 0x009A24C0, .decode = DECODE_GDDR_MRCODE, .dev_id = 0x2b85, .vram = "GDDR7", .arch = "GB202", .name = "RTX 5090" },
}; };
void gddr6_init(void) void gddr6_init(void)
@@ -91,7 +102,8 @@ int gddr6_detect_compatible_gpus(void)
ctx.devices = new_devices; ctx.devices = new_devices;
ctx.devices[ctx.num_devices] = dev_table[i]; ctx.devices[ctx.num_devices] = dev_table[i];
ctx.devices[ctx.num_devices].bar0 = (pci_dev->base_addr[0] & 0xffffffff); ctx.devices[ctx.num_devices].bar0 =
(uint64_t)(pci_dev->base_addr[0] & PCI_ADDR_MEM_MASK);
ctx.devices[ctx.num_devices].bus = pci_dev->bus; ctx.devices[ctx.num_devices].bus = pci_dev->bus;
ctx.devices[ctx.num_devices].dev = pci_dev->dev; ctx.devices[ctx.num_devices].dev = pci_dev->dev;
ctx.devices[ctx.num_devices].func = pci_dev->func; ctx.devices[ctx.num_devices].func = pci_dev->func;
@@ -109,9 +121,11 @@ void gddr6_memory_map(void)
for (uint32_t i = 0; i < ctx.num_devices; i++) for (uint32_t i = 0; i < ctx.num_devices; i++)
{ {
ctx.devices[i].phys_addr = (ctx.devices[i].bar0 + ctx.devices[i].offset); ctx.devices[i].phys_addr = (ctx.devices[i].bar0 + ctx.devices[i].offset);
ctx.devices[i].base_offset = ctx.devices[i].phys_addr & ~(PG_SZ - 1); ctx.devices[i].base_offset =
ctx.devices[i].phys_addr & ~((uint64_t)PG_SZ - 1);
ctx.devices[i].mapped_addr = mmap(0, PG_SZ, PROT_READ, MAP_SHARED, ctx.fd, ctx.devices[i].base_offset); ctx.devices[i].mapped_addr = mmap(0, PG_SZ, PROT_READ, MAP_SHARED,
ctx.fd, (off_t)ctx.devices[i].base_offset);
if (ctx.devices[i].mapped_addr == MAP_FAILED) if (ctx.devices[i].mapped_addr == MAP_FAILED)
{ {
ctx.devices[i].mapped_addr = NULL; ctx.devices[i].mapped_addr = NULL;
@@ -125,8 +139,77 @@ void gddr6_memory_map(void)
} }
} }
void gddr6_monitor_temperatures(void) // Convert a raw register value to degrees Celsius per the device's decode.
static int decode_temp(enum temp_decode decode, uint32_t raw)
{ {
switch (decode)
{
case DECODE_GDDR_MRCODE:
{
// GDDR temp MR-code in bits 23:16 (see NV_PFB_FBPA_DQR_STATUS_DQ).
// code 20 = 0 C, +2 C per unit above 20; below 20 is negative.
int code = (raw >> 16) & 0xFF;
if (code > 80) code = 80;
return (code > 19) ? (code - 20) * 2 : -(40 - code * 2);
}
case DECODE_ADA:
default:
return (raw & 0x00000fff) / 0x20;
}
}
// Blackwell GDDR7 per-memory-partition (module) DQR sensors. Module p lives at
// BAR0 + DQR_MODULE0 + p*DQR_STRIDE; validity nibble (all 4 IC/subp valid = 0xF)
// is at +DQR_VLD_OFF. Unlike the single pre-mapped register, these span several
// pages, so they are read on demand with a fresh page-aligned mmap.
#define DQR_MODULE0 0x009024C0u
#define DQR_VLD_OFF (0x009024D0u - 0x009024C0u) // +0x10
#define DQR_STRIDE 0x00004000u
#define DQR_MAX_MODULES 16
// Read one 32-bit MMIO register at BAR0+off via a fresh read-only page mmap.
// Returns 0 on success. Used only for the on-demand per-module GDDR7 reads.
static int read_bar0_reg(uint64_t bar0, uint32_t off, uint32_t *out)
{
long pg = PG_SZ;
uint64_t phys = (uint64_t)bar0 + off;
uint64_t base = phys & ~((uint64_t)pg - 1);
volatile void *map = mmap(0, pg, PROT_READ, MAP_SHARED, ctx.fd, (off_t)base);
if (map == MAP_FAILED) return -1;
*out = *(volatile uint32_t *)((const uint8_t *)map + (phys - base));
munmap((void *)map, pg);
return 0;
}
// Read the GDDR7 modules for a Blackwell device. Fills temps[]/present[] for up
// to DQR_MAX_MODULES, returns the module count found and the hottest temp in
// *hottest. A module counts as present only if all 4 DQR valid bits are set and
// the data word is not the 0xBADF.... poison sentinel.
static int gddr7_read_modules(uint64_t bar0, int temps[], int *hottest)
{
int count = 0, hot = -128;
for (int p = 0; p < DQR_MAX_MODULES; p++)
{
uint32_t off = DQR_MODULE0 + (uint32_t)p * DQR_STRIDE;
uint32_t vld = 0, dq = 0;
if (read_bar0_reg(bar0, off + DQR_VLD_OFF, &vld) != 0) continue;
if (read_bar0_reg(bar0, off, &dq) != 0) continue;
int all_valid = (((vld >> 24) & 0xF) == 0xF);
int poison = ((dq & 0xFFFF0000u) == 0xBADF0000u);
if (!all_valid || poison) continue;
int c = decode_temp(DECODE_GDDR_MRCODE, dq);
temps[count++] = c;
if (c > hot) hot = c;
}
*hottest = hot;
return count;
}
void gddr6_monitor_temperatures(int per_module)
{
int temps[DQR_MAX_MODULES];
while (1) { while (1) {
printf("\rVRAM Temps: |"); printf("\rVRAM Temps: |");
for (uint32_t i = 0; i < ctx.num_devices; i++) for (uint32_t i = 0; i < ctx.num_devices; i++)
@@ -136,11 +219,31 @@ void gddr6_monitor_temperatures(void)
continue; continue;
} }
// Blackwell GDDR7: per-module DQR sensors. Default shows the hotspot
// (max across modules); --per-module lists each module.
if (ctx.devices[i].decode == DECODE_GDDR_MRCODE)
{
int hottest = 0;
int n = gddr7_read_modules(ctx.devices[i].bar0, temps, &hottest);
if (n == 0) { printf(" n/a |"); continue; }
if (per_module)
{
for (int m = 0; m < n; m++)
printf(" m%d=%3d°C |", m, temps[m]);
}
else
{
printf(" %3d°C (hotspot) |", hottest);
}
continue;
}
// Ada/Ampere: single pre-mapped VRAM register.
void *virt_addr = (uint8_t *) ctx.devices[i].mapped_addr + (ctx.devices[i].phys_addr - ctx.devices[i].base_offset); void *virt_addr = (uint8_t *) ctx.devices[i].mapped_addr + (ctx.devices[i].phys_addr - ctx.devices[i].base_offset);
uint32_t read_result = *((uint32_t *)virt_addr); uint32_t read_result = *((uint32_t *)virt_addr);
uint32_t temp = ((read_result & 0x00000fff) / 0x20); int temp = decode_temp(ctx.devices[i].decode, read_result);
printf(" %3d°C |", temp);
printf(" %3u°C |", temp);
} }
fflush(stdout); fflush(stdout);
sleep(1); sleep(1);