65.9K
CodeProject 正在变化。 阅读更多。
Home

如何在 VBA/Excel 中使用 GPU

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.73/5 (8投票s)

2022 年 5 月 16 日

CPOL

9分钟阅读

viewsIcon

16615

downloadIcon

489

本文介绍了如何从 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.dllWindows 文件夹中(通常在每台 Windows 计算机上都存在)。

代码组织方式

  • 源代码包含包装器(用 C# 编写)和 VBA 代码的源文件。演示 Excel 表格未附加到源代码中。
  • 在“安装”后,可以在“C:\Program Files (x86)\ClooWrapperVBA\demo”文件夹中找到演示 Excel 表格。
  • 如果您想自己编译 C# 源代码,演示 Excel 表格可以在 zip 文件中的“demo”文件夹中找到。

让我们来燃烧 GPU/CPU!

通用说明

该 DLL 用 C# 编写,并包含一个 COM 接口,可以通过 Excel 访问 ClooCloo 是一个开源包装器,用于从 .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

请注意,SetPlatformSetDevice 函数在成功执行时返回 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 - 在上一步中从文件读取的纯文本源。
  • 第二个参数(“”)包含编译器选项。在最简单的情况下,它可以是空的(“”而不是 nullNothing)。除了常见的编译器选项,如“-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 日:初始版本
© . All rights reserved.