0%

对显存占用的监控(Windows系统)

前面说到了使用GPU加速文件搜索,但是会占用较多的显存,如果在打游戏,或者跑深度学习的情况下,并不会进行搜索,那么显存就被白白浪费掉了。因此如何实现显存的使用率监控,并实现检测到显存占用过多自动释放搜索缓存就成为了一个问题。

CUDA方面

在NVIDIA的文档中,有一个函数cudaMemGetInfo

1
__host__​cudaError_t cudaMemGetInfo ( size_t* free, size_t* total )

该方法可以直接获取GPU的空闲内存以及总内存。

但是经过实测,返回结果并不准确。因此并没有使用该方法。

OpenCL方面

在OpenCL中没有可以获取GPU占用的函数,但是由于OpenCL可以通过cl_device::getInfo获取显卡的各项参数,因此可以通过OpenCL提供的拓展参数来获取。

比如AMD显卡的OpenCL扩展中就有一项

1
2
3
4
#define CL_DEVICE_GLOBAL_FREE_MEMORY_AMD                0x4039

// 通过该方法即可获取AMD显卡的显存占用
cl_device::getInfo<CL_DEVICE_GLOBAL_FREE_MEMORY_AMD>();

AMD所有的OpenCL扩展在这里

https://registry.khronos.org/OpenCL/extensions/amd/cl_amd_device_attribute_query.txt

不过这样也有一些问题,比如适配困难。现在Intel也推出了自家的独立显卡Arc系列,不同显卡的适配就成为了一个问题。因此该方法也没有采用。

Windows任务管理器

其实在Windows任务管理器中就可以看到显存的占用,因此如何获取这个数值就成为了问题的关键。

l27CC.jpg

在Windows的powershell中有这样的API

Get-Counter (Microsoft.PowerShell.Diagnostics) - PowerShell | Microsoft Learn

通过Get-Counter可以实现获取系统中的各项参数。比如我们这里需要获取GPU相关的信息

l2enL.jpg

在Stack Overflow也找到了相同的解决方案

c++ - How to get the “Dedicated GPU memory” number for every running process in Windows (The same numbers that are shown in the Windows Task Manager) - Stack Overflow

由于powershell调用的效率太低,因此我们需要直接使用对应的Windows API,而不是重复调用powershell脚本。

也就是Windows API中的PDH函数

PDH函数

Using the PDH Functions to Consume Counter Data - Win32 apps | Microsoft Learn

通过PdhOpenQuery函数打开一个查询,然后使用PdhAddCounter设置要查询的信息,最后使用PdhCollectQueryData获取信息,再使用PdhGetRawCounterArray进行转换,即可获取我们想要的显存占用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
std::unordered_map<std::wstring, LONGLONG> query_pdh_val(PDH_STATUS& ret)
{
PDH_HQUERY query;
std::unordered_map<std::wstring, LONGLONG> memory_usage_map;
ret = PdhOpenQuery(nullptr, NULL, &query);
if (ret != ERROR_SUCCESS)
{
return memory_usage_map;
}
PDH_HCOUNTER counter;
ret = PdhAddCounter(query, L"\\GPU Adapter Memory(*)\\Dedicated Usage", NULL, &counter);
if (ret != ERROR_SUCCESS)
{
return memory_usage_map;
}
ret = PdhCollectQueryData(query);
if (ret != ERROR_SUCCESS)
{
return memory_usage_map;
}
DWORD bufferSize = 0;
DWORD itemCount = 0;
PdhGetRawCounterArray(counter, &bufferSize, &itemCount, nullptr);
auto&& lpItemBuffer = reinterpret_cast<PPDH_RAW_COUNTER_ITEM_W>(new char[bufferSize]);
ret = PdhGetRawCounterArray(counter, &bufferSize, &itemCount, lpItemBuffer);
if (ret != ERROR_SUCCESS)
{
delete[] lpItemBuffer;
return memory_usage_map;
}
for (DWORD i = 0; i < itemCount; ++i)
{
auto& [szName, RawValue] = lpItemBuffer[i];
memory_usage_map.insert(std::make_pair(szName, RawValue.FirstValue));
}
delete[] lpItemBuffer;
ret = PdhCloseQuery(query);
return memory_usage_map;
}

这里的query_pdh_val()方法返回的是GPU的luid和显存占用的map。

和powershell返回的结果是相同的

l2t1q.jpg

但是这里又出现了一个大问题,这些luid开头的东西是什么,和显卡是怎么进行对应的。

我们平时看到的显卡名都是像NVIDIA GeForce RTX 2060,AMD Radeon RX 5700XT这样

l2vpx.jpg

powershell中返回的gpu adapter memory(luid_0x00000000_0xXXXXXXXX)到底是如何和显卡一一对应的仍然不清楚。

GPU名称和luid的对应查询方案

在网上找了非常多的资料,都没有找到。最后是在翻看Windows DirectX API中偶然发现了在DXGI_ADAPTER_DESC结构体中有一个字段,名字就叫AdapterLuid

1
2
3
4
5
6
7
8
9
10
11
typedef struct DXGI_ADAPTER_DESC {
WCHAR Description[128];
UINT VendorId;
UINT DeviceId;
UINT SubSysId;
UINT Revision;
SIZE_T DedicatedVideoMemory;
SIZE_T DedicatedSystemMemory;
SIZE_T SharedSystemMemory;
LUID AdapterLuid;
} DXGI_ADAPTER_DESC;

因此,只需要通过DirectX API将显卡和luid对应起来,再和前面的PDH函数查询出的信息进行对应,即可实现显卡和显存占用信息的对应。

具体的方法为如下:

创建DXGIFactory,这里由于是jni调用的,所以我抛出了一个Java的异常。

1
2
3
4
5
if (CreateDXGIFactory(__uuidof(IDXGIFactory), reinterpret_cast<void**>(&p_dxgi_factory)) != S_OK)
{
env->ThrowNew(env->FindClass("java/lang/Exception"), "Create dxgi factory failed.");
return;
}

然后使用DXGIFactory遍历所有GPU,即可获取GPU名称和luid的对应关系

1
2
3
4
5
6
7
8
for (UINT i = 0;
p_dxgi_factory->EnumAdapters(i, &p_adapter) != DXGI_ERROR_NOT_FOUND;
++i)
{
DXGI_ADAPTER_DESC adapter_desc;
p_adapter->GetDesc(&adapter_desc);
gpu_name_adapter_map.insert(std::make_pair(adapter_desc.Description, adapter_desc));
}

将对应关系保存至gpu_name_adapter_map中。

Luid有两个成员,一个是LowPart,一个是HighPart

1
2
3
4
typedef struct _LUID {
DWORD LowPart;
LONG HighPart;
} LUID, *PLUID;

在Windows系统中是高位在前,低位在后,因此通过下面的方法即可获取luid,n2hexstr()方法是数字转16进制字符串。

1
2
3
4
5
6
7
8
9
10
11
template <typename I>
std::string n2hexstr(I w, size_t hex_len = sizeof(I) << 1)
{
static const char* digits = "0123456789ABCDEF";
std::string rc(hex_len, '0');
for (size_t i = 0, j = (hex_len - 1) * 4; i < hex_len; ++i, j -= 4)
rc[i] = digits[w >> j & 0x0f];
return rc;
}

auto&& luid_str = "0x" + n2hexstr(adapter_luid.HighPart) + "_" + "0x" + n2hexstr(adapter_luid.LowPart);

在我这台电脑上,例如想监控NVIDIA GeForce RTX 2060的显卡的显存占用。先通过gpu_name_adapter_map找到对应的luid,为0x00000000_0x00010fcf,然后调用query_pdh_val()函数获取所有显卡的显存占用,最后通过luid找出对应的显存占用即可。

至此,对显卡显存占用的查询监控方案结束。