如何在 VBA/Excel 中使用 GPU






4.73/5 (8投票s)
本文介绍了如何从 VBA 编译和执行 OpenCL 代码。
引言
OpenCL 是一种基于 C99 的语言,用于对 GPGPU 和 CPU 进行编程(维基百科中还提到了 DSP 和 FPGA)。OpenCL 的优点在于,同一段代码无需任何修改即可在 GPGPU 和 CPU 上执行,并且支持的平台数量庞大。另一方面,对于非专业人士来说,最广泛使用的编程语言 VBA,只支持在单个处理器上进行计算,并且无法异步执行代码。
我非常喜欢 Excel 的交互性,但有时进行大规模计算需要更多的处理能力。因此,我希望本文能弥补这一不足,并展示如何为 Excel 添加对 GPGPU/CPU 上多平台/多线程计算的支持。
这篇短文的目的是展示如何将 OpenCL 的支持添加到 VBA。由于我的需求仅限于提高 Excel 的计算能力,因此没有实现/讨论图像处理及相关主题。对于那些喜欢“开箱即用”解决方案的人来说,有一个(仅限 Windows)安装程序,可以安装 ClooWrapperVBA
库并进行注册。对于不信任安装程序的人,同样的内容也以 zip 文件形式提供。您只需要使用“register.bat”注册 DLL 即可。
谁会受益于这个库?
- 用 Excel 制作模型的业余爱好者。这正是我!
- 想学习 OpenCL 但又不想安装 Visual Studio 等附加程序的人。
- 老派科学家,他们在 Excel 中进行研究。我听说过那些武士。
要求
- Windows
- .NET 4.0 框架
- 已安装 Excel
- OpenCL.dll 在 Windows 文件夹中(通常在每台 Windows 计算机上都存在)。
代码组织方式
- 源代码包含包装器(用 C# 编写)和 VBA 代码的源文件。演示 Excel 表格未附加到源代码中。
- 在“安装”后,可以在“C:\Program Files (x86)\ClooWrapperVBA\demo”文件夹中找到演示 Excel 表格。
- 如果您想自己编译 C# 源代码,演示 Excel 表格可以在 zip 文件中的“demo”文件夹中找到。
让我们来燃烧 GPU/CPU!
通用说明
该 DLL 用 C# 编写,并包含一个 COM 接口,可以通过 Excel 访问 Cloo。Cloo 是一个开源包装器,用于从 .NET 执行 OpenCL 代码。
你好世界!
让我们从一个简单的程序开始,该程序执行用 OpenCL 编写的矩阵乘法代码。
Dim clooConfiguration As New ClooWrapperVBA.Configuration
首先,检查我们可用的硬件会很有趣。下面的代码片段遍历每个平台的所有可用设备,并获取它们的配置。
nPlatforms = clooConfiguration.Platforms
For i = 1 To nPlatforms
result = clooConfiguration.SetPlatform(i - 1)
If result Then
platformName = clooConfiguration.Platform.PlatformName
platformVendor = clooConfiguration.Platform.PlatformVendor
platformVersion = clooConfiguration.Platform.PlatformVersion
nDevices = clooConfiguration.Platform.Devices
For j = 1 To nDevices
result = clooConfiguration.Platform.SetDevice(j - 1)
If result Then
deviceType = clooConfiguration.Platform.Device.DeviceType
deviceName = clooConfiguration.Platform.device.DeviceName
deviceVendor = clooConfiguration.Platform.device.DeviceVendor
maxComputeUnits = clooConfiguration.Platform.device.MaxComputeUnits
deviceAvailable = clooConfiguration.Platform.device.DeviceAvailable
compilerAvailable = clooConfiguration.Platform.device.CompilerAvailable
deviceVersion = clooConfiguration.Platform.device.DeviceVersion
driverVersion = clooConfiguration.Platform.device.DriverVersion
globalMemorySize = clooConfiguration.Platform.device.GlobalMemorySize
maxClockFrequency = clooConfiguration.Platform.device.MaxClockFrequency
maxMemoryAllocationSize = _
clooConfiguration.Platform.device.MaxMemoryAllocationSize
openCLCVersionString = _
clooConfiguration.Platform.device.OpenCLCVersionString
End If
Next j
End If
Next i
重要的配置设置是
deviceType
(“GPU”/“CPU”)maxComputeUnits
- 可用的处理器/线程数deviceAvailable
compilerAvailable
请注意,SetPlatform
和 SetDevice
函数在成功执行时返回 true
,未成功执行时返回 false
。
如果没有找到平台或设备,请检查 OpenCL.dll 是否存在于“Windows”文件夹中(通常在“C:\Windows\”)。如果“Windows”文件夹中没有 OpenCL.dll,请从其他计算机复制或自行搜索。无论如何都要进行杀毒检查!另一个可能的原因是 GPGPU/CPU 驱动程序太旧,不支持 OpenCL。请搜索您的 CPU/GPGPU 的最新驱动程序。
OpenCL 中的矩阵乘法
现在,让我们使用第一个可用的能够编译 OpenCL 源的设备来计算两个浮点矩阵 M1[p, q] 和 M2[q, r] 的乘积。
首先,将读取 OpenCL 源。
Open Application.ActiveWorkbook.Path & "\cl\MatrixMultiplication.cl" For Binary As #1
sources = Space$(LOF(1))
Get #1, , sources
Close #1
下面是计算两个浮点矩阵乘积的 OpenCL 代码。
__kernel void FloatMatrixMult_
(__global float* MResp, __global float* M1, __global float* M2, __global int* q)
{
// Vector element index
int i = get_global_id(0);
int j = get_global_id(1);
int p = get_global_size(0);
int r = get_global_size(1);
MResp[i + p * j] = 0;
int QQ = q[0];
for (int k = 0; k < QQ; k++)
{
MResp[i + p * j] += M1[i + p * k] * M2[k + QQ * j];
}
}
如果找到的设备有编译器(compilerAvailable
= true
),我们将尝试编译 OpenCL 源。
Set progDevice = New ClooWrapperVBA.ProgramDevice
result = progDevice.Build(sources, "", platformId, deviceId, cpuCounter, buildLogs)
参数如下:
sources
- 在上一步中从文件读取的纯文本源。- 第二个参数(“”)包含编译器选项。在最简单的情况下,它可以是空的(“”而不是
null
或Nothing
)。除了常见的编译器选项,如“-w
”(禁止所有警告消息)之外,您还可以在此处定义常用的常量(“-D
name=definition”)并在 OpenCL 代码中使用它们。完整的编译器选项列表可以在 Khronos 网页上找到。 - 第五个参数
cpuCounter
定义了某种特定类型(“CPU
”或“GPU
”)的设备索引。由于您的平台可能拥有数千个不同类型的设备,因此此参数用于区分同一类型的不同设备。 buildLogs
显示当前的编译器日志。检查其中是否包含任何错误非常重要。在我开发第一个自己的 OpenCL 代码时,检查构建日志中的警告对我非常有帮助。警告通常是内核在执行期间崩溃的原因。错误也会累积在ErrorString
属性中。
errorString = progDevice.ErrorString
如果编译成功(result
= true
),我们必须定义要执行的内核。CreateKernel
函数的单个参数是内核名称(string
)。
result = progDevice.CreateKernel("DoubleMatrixMult")
然后,我们必须设置内核的输入和输出数组。OpenCL 通常使用向量(一维数组)作为输入参数。
result = progDevice.SetMemoryArgument_Double(0, vecResp)
result = progDevice.SetMemoryArgument_Double(1, vecM1)
result = progDevice.SetMemoryArgument_Double(2, vecM2)
result = progDevice.SetMemoryArgument_Long(3, vecQ)
有六个函数用于设置内核参数:
- 设置数组:
SetMemoryArgument_Long
(对应 C# 中的整数数组)SetMemoryArgument_Single
(对应 C# 中的浮点数数组)SetMemoryArgument_Double
- 设置值:
SetValueArgument_Long
(对应 C# 中的整数)SetValueArgument_Single
(对应 C# 中的浮点数)SetValueArgument_Double
第一个参数,参数索引,从 0
开始表示第一个参数,并且必须为后续参数递增。同样重要的是以正确的顺序设置变量。首先是参数索引为 0
的变量,然后是参数索引为 1
的变量,依此类推。
现在,必须在 globalWorkSize
数组中设置输入数组的大小。
globalWorkSize(0) = p
globalWorkSize(1) = r
最后,我们可以开始内核执行。ExecuteSync
函数仅在执行完成后才返回到 VBA。如果您想在多个设备上以异步模式运行 OpenCL 代码,则需要阅读“**高级主题**”。
result = progDevice.ExecuteSync(globalWorkOffset, globalWorkSize, localWorkSize)
执行结果必须使用相应的 Get
函数从内核中获取。
result = progDevice.GetMemoryArgument_Double(0, vecResp)
第一个参数,参数 index
,与“Set
”函数的参数索引参数的含义相同,但您可以使用“Get
”函数以任意顺序。
最后,内存应从数组中清理,并清理所有实例化的 OpenCL
对象。
result = progDevice.ReleaseMemObject(3)
result = progDevice.ReleaseMemObject(2)
result = progDevice.ReleaseMemObject(1)
result = progDevice.ReleaseMemObject(0)
result = progDevice.ReleaseKernel
result = progDevice.ReleaseProgram
释放函数必须按以下顺序调用:
- 释放内存参数,从最高的参数索引开始。
- 释放内核。
ReleaseProgram
最后,请注意,DLL 的所有 COM 可见函数都返回布尔值:如果函数执行成功,则返回 true
;如果失败,则返回 false
。如果任何函数返回 false
,则检查 ErrorString
属性中的错误会很有帮助。
高级主题 (Advanced Topics)
前面详细讨论的基本示例仅包含 Cloo 可能性的很小一部分。ClooWrapperVBA
的完整可能性可以在相应的 Excel 工作表中进行测试(“Configuration
”、“Performance
”、“Asynchronous
”)。
配置
只需按“**Configuration**”按钮,您就会了解系统的真实情况。:-)
性能测量
该工作表使用文章“如何在 .NET 中使用您的 GPU”中的 OpenCL 代码进行简单的性能测量。性能测试在第一个找到的 CPU 和 GPGPU 设备上以单精度和双精度进行。
另一项性能测试是对 VBA 中的两个 1200*1200 双精度矩阵与第一个找到的 CPU/GPGPU 进行乘法运算。CPU/GPGPU 上的计算结果与 VBA 结果进行比较(见单元格 C3:C4),并测量了计算时间(单元格 B2:B4)。结果显示,在 GPGPU 上执行的 OpenCL 代码比 VBA 快 300 倍(单元格 B9),也比原生 C# 代码快 8 倍!(可以想象,我的 Excel 比原生 C# 快 8 倍!!!)“原生”计算是用 C# 完成的,并执行相同的两个 1200*1200 矩阵乘法。原生性能测量仅在我自己的计算机上进行,因此保持不变。如果您愿意,可以将其用任何您想要的编程语言(C#/C/C++)重写,并手动更新执行时间。我没有将其添加到 ClooWrapperVBA
中,是因为这是一个单独的测试用例,我只想要一个干净的 ClooWrapperVBA
源。
OpenCL 代码的异步执行
有两个额外的函数用于以异步模式执行内核:
1. ExecuteBackground
result = programDevice.ExecuteBackground_
(globalWorkOffset, globalWorkSize, localWorkSize, THREAD_PRIORITY)
在下面的代码片段中,OpenCL 代码在 CPU 和 GPGPU 上同时以无限循环执行。
- 每 100 毫秒,使用
ExecutionCompleted
函数检查 CPU/GPGPU 上的执行状态(true
- 执行完成,false
- OpenCL 代码仍在运行)。 - 如果执行完成,将从内核读取输出数组,将新输入数组写入内核,并使用
ExecuteBackground
在所需设备上启动 OpenCL 代码。 - 无限循环将一直运行,直到达到
MAX_TASKS
(=20
)。
While Not allTasks_Completed
For i = 1 To progDevices.Count
If progDevices.Item(i).ProgramDevice.ExecutionCompleted Then
result = progDevices.Item(i).ProgramDevice.GetMemoryArgument_Double_
(0, vecResp) ' Extract the results and do something with received data here.
finishedTasks = finishedTasks + 1
' Start new task
If startedTasks < MAX_TASKS Then
ReDim vecResp(UBound(vecResp)) ' Erase output vector.
result = progDevices.Item(i).ProgramDevice.SetMemoryArgument_Double_
(0, vecResp)
' If you want to use callbacks, than use function below
' "CPU_Task_Completed" is a function that will obtain the callback.
' Call progDevices.Item(i).ProgramDevice.ExecuteAsync_
(globalWorkOffset, globalWorkSize, localWorkSize, _
THREAD_PRIORITY, AddressOf Asynchronous.CPU_Task_Completed)
result = progDevices.Item(i).ProgramDevice.ExecuteBackground_
(globalWorkOffset, globalWorkSize, localWorkSize, THREAD_PRIORITY)
startedTasks = startedTasks + 1
currentTaskId(i) = startedTasks
Else
' If the maximal number of tasks is reached,
' then set "ExecutionCompleted" to false to avoid additional outputs.
progDevices.Item(i).ProgramDevice.ExecutionCompleted = False
End If
If startedTasks = finishedTasks Then
allTasks_Completed = True
End If
End If
Next i
DoEvents
Sleep (100)
Wend
2. ExecuteAsync
result = programDevice.ExecuteAsync(globalWorkOffset, globalWorkSize, _
localWorkSize, THREAD_PRIORITY, AddressOf Asynchronous.CPU_Task_Completed)
- Excel 演示表的早期版本使用
ExecuteAsync
函数以异步模式运行 OpenCL 代码。该函数在执行完成时回调到 VBA 函数。但后来我发现调试使用回调函数的代码相当困难。例如,如果您当前处于调试器中,并且代码接收到回调,Excel 可能会崩溃。即使是向工作表写入结果也可能导致 Excel 崩溃。因此,在测试工作表中,我没有使用此函数,尽管它仍然在 COM 接口中。
这两个函数的通用参数 THREAD_PRIORITY
设置了五个优先级之一(0 - “Lowest
”,1 - “BelowNormal
”,2 - “Normal
”,3 - “AboveNormal
”,4 - “Highest
”),但我很确定此参数对 OpenCL 代码的执行没有影响。我只是添加它,因为我有这种可能性 :-)
该工作表初始化 OpenCL 程序,在第一个找到的 CPU 和 GPGPU 设备上计算两个 2000*2000 矩阵,并以异步模式执行 20 次。因此,每次都在 CPU 和 GPGPU 上异步执行程序。执行状态在工作表中显示。在我的计算机上,完成的 CPU 计算数量是 GPGPU 进程数量的 4 倍,这与之前的性能测量结果一致。
从 VBScript 运行 ClooWrapperVBA
实际上,VBScript 可以使用 COM-DLL 并不让我感到惊讶,但仍然有些出乎意料。ClooWrapperVBA
可以获取平台/设备配置并将其写出。目前无法使用 ClooWrapperVBA
进行计算,因为 VBScript 中的变量是 Variant
类型,必须使用 ArrayList
发送到 DLL,这会使 DLL 更加复杂。由于 VBScript 的使用并非本意,因此在 VBScript 中只能使用 ClooWrapperVBA
来写出您当前的平台/设备配置。
结论和关注点
源代码可在 我的 GitHub 页面上找到,如果您(是的,尤其是您!)创建一个拉取请求,我将非常高兴。任何改进都将受到高度赞赏!
历史
- 2022 年 5 月 15 日:初始版本