~blog

Récupérer la température d'un GPU NVIDIA d'une GTX 1080 sous Linux, sans nouveau ni nvidia_drm

Posted at — mars 3, 2023

 
J’ai quelques GPUs NVIDIA (~25 000) en passthrough sur des VMs, mais j’ai besoin monitorer leur température depuis l’hyperviseur (qemu). Pas de nvidia_smi disponible, pas de driver nouveau ou nvidia_drm de chargé, ici seulement du vfio-pci et qemu.
 
Du coup, j’ai creusé pour pouvoir récupérer leurs températures depuis l’hyperviseur !
 
Tous les tests seront menés sur une GTX 1080 (GP104). Cette technique fonctionne sur les familles Pascal et Ampère. La même logique possiblement peut être appliqué pour les autres familles, cependant je n’ai pas de quoi tester.
 

Où commencer ?

Le site d’envytools permet de commencer la recherche et de trouver quelques pistes, dont les ranges des registres MMIO (memory mapped i/o) ainsi que la description des PCI BARs (base register address).
Notamment ce registre PTHERM.
 
L’autre piste n’est que le kernel en lui même, en espérant que le driver nouveau puisse récupérer la température du GPU (spoiler: oui). Il suffit de creuser le code afin de mieux comprendre ce qu’il peut se passer.
Dans tous les cas, une idée plus précise se creuse: lire dans la mémoire du GPU, quelque part.

Un peu de lecture

Extrait du code pour récupérer la température du GPU:
source: linux/drivers/gpu/drm/nouveau/nvkm/subdev/therm/gp100.c

static int
gp100_temp_get(struct nvkm_therm *therm)
{
        struct nvkm_device *device = therm->subdev.device;
        struct nvkm_subdev *subdev = &therm->subdev;
        u32 tsensor = nvkm_rd32(device, 0x020460);
        u32 inttemp = (tsensor & 0x0001fff8);

        /* device SHADOWed */
        if (tsensor & 0x40000000)
                nvkm_trace(subdev, "reading temperature from SHADOWed sensor\n");

        /* device valid */
        if (tsensor & 0x20000000)
                return (inttemp >> 8);
        else
                return -ENODEV;
} 

source: linux/drivers/gpu/drm/nouveau/include/nvkm/core/device.h

// Struct réduire pour lisibilité
struct nvkm_device {
    [...]
    struct device *dev;
    const char *name;
    [...]
    void __iomem *pri;
    [...]
    struct list_head subdev;
    [...]
};
[...]

#define nvkm_rd32(d,a) ioread32_native((d)->pri + (a))

Une valeur du sensor est récupérée via la macro nvkm_rd32, qui est une lecture sur un registre de 32bits, à l’offset device->pri + 0x020460.
Des masques sont ainsi appliqués à la valeur du sensor pour avoir seulement la température. Il faut garder tout ça de côté.
 
source: linux/drivers/gpu/drm/nouveau/nkvm/engine/device/base.c

    [...]
    mmio_base = device->func->resource_addr(device, 0);
    mmio_size = device->func->resource_size(device, 0);
    [...]
    device->pri = ioremap(mmio_base, mmio_size);    

ioremap va mapper le MMIO sur un espace mémoire virtuel et va retourner un pointeur vers le début de cet espace correspondant au début de la mémoire physique du GPU à l’addresse demandée. D’où l’utilisation de ioread32 puisqu’il est déconseillé de dé-référencer directement ces adresses.
 
À quoi correspond mmio_base et mmio_size ? Sachant d’après la doc d’envy, l’espace mémoire des registres MMIO se trouvent sur la BAR 0 et fait 16M. C’est une zone d’adressage mémoire en 32bits qui est non-prefetchable.
 
La mmio_base corresponds donc à l’addresse de début de la BAR 0 et la mmio_size est de 16M. Cela peut aussi se vérifier avec lspci.

# lspci -d 10de: -s .0 -vv
02:00.0 VGA compatible controller: NVIDIA Corporation GP104 [GeForce GTX 1080] (rev a1) (prog-if 00 [VGA controller])
Subsystem: ZOTAC International (MCO) Ltd. GP104 [GeForce GTX 1080]
Control: I/O+ Mem+ BusMaster- SpecCycle- MemWINV- VGASnoop- ParErr- Stepping- SERR+ FastB2B- DisINTx-
Status: Cap+ 66MHz- UDF- FastB2B- ParErr- DEVSEL=fast >TAbort- <TAbort- <MAbort- >SERR- <PERR- INTx-
Interrupt: pin A routed to IRQ 26
NUMA node: 0
IOMMU group: 45
//   BAR 0         mmio_base                                mmio_size
//     v               v                                       v
Region 0: Memory at c6000000 (32-bit, non-prefetchable) [size=16M]
Region 1: Memory at 27fe0000000 (64-bit, prefetchable) [size=256M]
Region 3: Memory at 27ff0000000 (64-bit, prefetchable) [size=32M]
Region 5: I/O ports at 6000 [size=128]
Expansion ROM at c7000000 [disabled] [size=512K]
[...]
Kernel driver in use: vfio-pci

En pratique

Désormais il faut écrire un petit bout de code qui permet d’accéder à mon GPU via vfio, et lire à l’offset récupéré précédemment une valeur dans le sensor et appliquer les masques afin d’en extraire juste la température du GPU.
 
Le code est assez basique et provient de la doc kernel sur vfio. Dans cet exemple je veux accèder au device 02:00.0 qui se trouve dans le groupe IOMMU 45 (vu dans le lspci ci dessus).

#include <linux/vfio.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <stdint.h>
#include <stdio.h>
#include <errno.h>

int main() {
  int container, group, device, i;
 struct vfio_group_status group_status =
   { .argsz = sizeof(group_status) };
 struct vfio_iommu_type1_info iommu_info = { .argsz = sizeof(iommu_info) };
 struct vfio_iommu_type1_dma_map dma_map = { .argsz = sizeof(dma_map) };
 struct vfio_device_info device_info = { .argsz = sizeof(device_info) };

 /* Create a new container */
 container = open("/dev/vfio/vfio", O_RDWR);

 if (ioctl(container, VFIO_GET_API_VERSION) != VFIO_API_VERSION)
   return -1;
 /* Unknown API version */

 if (!ioctl(container, VFIO_CHECK_EXTENSION, VFIO_TYPE1_IOMMU))
   return -1;
 /* Doesn't support the IOMMU driver we want. */

 /* Open the group */
 group = open("/dev/vfio/45", O_RDONLY);
 if (group < 0)
   return -1;

 /* Test the group is viable and available */
 ioctl(group, VFIO_GROUP_GET_STATUS, &group_status);

 if (!(group_status.flags & VFIO_GROUP_FLAGS_VIABLE))
   /* Group is not viable (ie, not all devices bound for vfio) */
   return -1;

 /* Add the group to the container */
 ioctl(group, VFIO_GROUP_SET_CONTAINER, &container);

 /* Enable the IOMMU model we want */
 ioctl(container, VFIO_SET_IOMMU, VFIO_TYPE1_IOMMU);

 /* Get addition IOMMU info */
 ioctl(container, VFIO_IOMMU_GET_INFO, &iommu_info);

 /* Allocate some space and setup a DMA mapping */
 dma_map.vaddr = mmap(0, 1024 * 1024, PROT_READ | PROT_WRITE,
		      MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
 dma_map.size = 1024 * 1024;
 dma_map.iova = 0; /* 1MB starting at 0x0 from device view */
 dma_map.flags = VFIO_DMA_MAP_FLAG_READ | VFIO_DMA_MAP_FLAG_WRITE;

 ioctl(container, VFIO_IOMMU_MAP_DMA, &dma_map);

 /* Get a file descriptor for the device */
 device = ioctl(group, VFIO_GROUP_GET_DEVICE_FD, "0000:02:00.0");

 /* Test and setup the device */
 ioctl(device, VFIO_DEVICE_GET_INFO, &device_info);

 /* Working only on BAR 0 */
 struct vfio_region_info regs = {
   .argsz = sizeof(struct vfio_region_info),
   .index  = 0
 };
 
 ioctl(device, VFIO_DEVICE_GET_REGION_INFO, ®s);
 
 uint8_t* ptr = mmap(0, regs.size, PROT_READ, MAP_SHARED, device, 0);

 /* Stolen from you know where ;) */
 uint32_t tsensor = *(uint32_t*)(ptr + 0x020460);
 uint32_t inttemp = (tsensor & 0x0001fff8);
 
 if (tsensor & 0x40000000)
   printf("shadowed sensor\n");

 if (tsensor & 0x20000000)
   printf("temp %d\n", inttemp >> 8);

 /* Gratuitous device reset and go... */
 ioctl(device, VFIO_DEVICE_RESET);
 munmap(ptr, regs.size);

 return 0;
}
# gcc vfio.c && ./a.out
temp 28

Ici on voit bien que le GPU est donc à 28°C (ils sont watercoolés)
Dernier problème malheureusement, c’est que si le GPU est déjà utilisé, par exemple via qemu, le open sur groupe d’IOMMU va renvoyer qu’il est déjà utilisé.
Cette fois-ci il existe une autre solution, qui consiste directement à taper dans la mémoire avec qemu monitor, avec la commande xp.
 
Je démarre une VM sur ce GPU, et en utilisant l’adresse de base récupérée de la BAR0 (avec info pci), nous allons y accéder en ajoutant l’offset de la position du registre contenant la température:

(qemu) info pci
[...]
  Bus  2, device   0, function 0:
    VGA controller: PCI device 10de:1b80
      PCI subsystem 19da:1425
      IRQ 0, pin A
      BAR0: 32 bit memory at 0xc1000000 [0xc1ffffff].
      BAR1: 64 bit prefetchable memory at 0x1000000000 [0x100fffffff].
      BAR3: 64 bit prefetchable memory at 0x1010000000 [0x1011ffffff].
      BAR5: I/O at 0xb000 [0xb07f].
      BAR6: 32 bit memory at 0xffffffffffffffff [0x0007fffe].
[..]
# On lit un registre 32bits à la position 0xc1020460
(qemu) xp /1w (0xc1000000 + 0x020460)
00000000c1020460: 0x20001ca0
# On fait les shifts récupérés via le code dans nouveau
In [1]: (0x20001ca0 & 0x0001fff8) >> 8
Out[1]: 28 <-- la temperature

Conclusion

Avec tout ça il est possible de monitorer très simplement la température de ces GPU depuis l’host, en utilisant ces deux techniques, à condition de ne pas interférer avec le lancement de la VM. Cependant ce n’est pas fini, sur certains modèles il y a encore d’autres sensors à récupérer…
 
Ce fut un petit projet assez fun, peut-être je tenterais sur d’autres familles de GPU NVIDIA si je peux mettre la main dessus, voire même peut-être des GPUs AMD.