3. MUSA 软件架构
MUSA(Meta-computing Unified System Architecture),中文名为元计算统一系统架构,是显卡厂商摩尔线程(Moore Threads)推出的运算平台。元计算一词涵盖人工智能计算,图形计算,物理仿真计算等重要领域。MUSA 是一种通用并行计算架构,该架构使 GPU 能够解决复杂的计算问题。 它包含了 MUSA 指令集架构(ISA)以及 GPU 内部的并行计算引擎。
MUSA 带有一个软件环境,允许开发人员使用 C/C++ 语言为 MUSA 架构编写程序,编写出的程序可以在支持 MUSA 的 GPU 上以超高性能运行。同时 MUSA 软件栈还提供了各个领域的加速库,供开发者直接使用,便于快速适配各种高性能计算场景。具体如图 1 所示。

图1 MUSA 软件栈
3.1. MUSA 开发套件
MUSA 开发套件(MUSA Toolkit)是一套用于开发、优化及部署高性能 GPU 加速应用程序的工具包,它包含了 GPU 加速库、调试优化工具、C/C++ 编译器以及用于部署应用程序的运行时库。
借助 MUSA Toolkit,开发者可以将自己的应用程序快速部署到嵌入式系统、桌面工作站、企业数据中心以及超级计算机上。并且 MUSA Toolkit 具有跨多 GPU 的分布式计算能力,支持将应用程序从单个 GPU 工作站扩展到具有数千个 GPU 的云平台上。
3.2. MUSA 驱动与运行时库
3.2.1. 初始化函数
MUSA 运行时库(MUSA Runtime)没有特定的初始化函数(Initialization),在程序第一次调用 Runtime 库函数时会自动完成初始化。因此在记录 Runtime 函数调用时间和解释程序中第一个 Runtime 调用返回的错误代码时,需要将初始化考虑在内。
在初始化期间,Runtime 将会为系统中每一个设备创建一个 MUSA context,这个 context 是设备的基础 context(primary context),它被程序中所有的主机线程所共享。创建过程在后台运行,并且 Runtime 将隐藏 primary context,使之对 Runtime API 这一层的程序员不可见。
当一个主机线程调用 musaDeviceReset() 函数时,它将会销毁线程当前控制设备的 primary context。即当线程下一次调用 Runtime 函数时将会重启初始化,一个新的 MUSA primary context 将被创建出来。
3.2.2. 设备内存
MUSA 编程模型假定系统由主机侧(host) 和设备侧(device) 构成,它们分别具有独立的内存空间。Runtime 负责设备内存(Device Memory)的分配,回收,拷贝以及在主机和设备之间传输数据的工作。
设备内存可以有两种分配方式:线性内存 (Linear Memory) 或者 MUSA 数组 (MUSA Arrays)。
MUSA 数组是一块不透明的内存空间,它主要用于纹理存取优化。
线性内存空间与平时我们访问的内存类似,对于计算能力 1.x 的设备来说,它存在于一个 32 位的地址空间。对于更高计算能力的设备而言,它存在于一个 40 位的地址空间中。因此,单独分配的实体可以使用指针来相互应用。
线性内存空间和MUSA数组空间的内容在对用户可见时,不保证已被清零。
通常,我们使用 musaMalloc() 函数分配线性内存空间。释放线性内存空间时调用 musaFree() 函数,musaMemcpy() 函数用于主机和设备之间传输数据。以下是 musa Vector Add 代码示例的一些片段:
#include <iostream>
__global__ void axpy(float *x, float *y, float a) {
y[threadIdx.x] = a * x[threadIdx.x];
}
int main(int argc, char *argv[]) {
const int kDataLen = 4;
float a = 2.0f;
float host_x[kDataLen] = {1.0f, 2.0f, 3.0f, 4.0f};
float host_y[kDataLen];
// Copy input data to device.
float *device_x;
float *device_y;
musaMalloc(&device_x, kDataLen * sizeof(float));
musaMalloc(&device_y, kDataLen * sizeof(float));
musaMemcpy(device_x, host_x, kDataLen * sizeof(float),
musaMemcpyHostToDevice);
// Launch the kernel.
axpy<<<1, kDataLen>>>(device_x, device_y, a);
// Copy output data to host.
musaDeviceSynchronize();
musaMemcpy(host_y, device_y, kDataLen * sizeof(float),
musaMemcpyDeviceToHost);
// Print the results.
for (int i = 0; i < kDataLen; ++i) {
std::cout << "y[" << i << "] = " << host_y[i] << "\n";
}
musaFree(device_x);
musaFree(device_y);
return 0;
}
上述代码展示了设备内存的分配,传输以及回收过程。
除了上面展示的方法,我们还可以使用 musaMallocPitch() 和 musaMalloc3D() 函数来分配线性内存。这些函数能够确保分配的内存满足设备内存访问的对齐要求,对行地址的访问以及多维数组间的数据传输提供高性能保证,因此非常适用于对二维和三维数组内存空间的分配。下面的代码片段展示了分配和使用尺寸为 width x height 的二维数组的技术:
// Host code
int width = 64, height = 64;
float* devPtr;
size_t pitch;
musaMallocPitch(&devPtr, &pitch,
width * sizeof(float), height);
MyKernel<<<100, 512>>>(devPtr, pitch, width, height);
// Device code
__global__ void MyKernel(float* devPtr,
size_t pitch, int width, int height)
{
for (int r = 0; r < height; ++r) {
float* row = (float*)((char*)devPtr + r * pitch);
for (int c = 0; c < width; ++c) {
float element = row[c];
}
}
}
更多详细的内容请参考 MUSA Runtime API Reference。
下面的代码示例展示了多种使用 Runtime API 访问全局变量的技术:
__constant__ float constData[256];
float data[256];
musaMemcpyToSymbol(constData, data, sizeof(data));
musaMemcpyFromSymbol(data, constData, sizeof(data));
__device__ float devData;
float value = 3.14f;
musaMemcpyToSymbol(devData, &value, sizeof(float));
__device__ float* devPointer;
float* ptr;
musaMalloc(&ptr, 256 * sizeof(float));
musaMemcpyToSymbol(devPointer, &ptr, sizeof(ptr));
使用 musaGetSymbolAddress() 函数可以获得被声明存储在全局内存中的变量地址。如果需要获取分配内存的大小,可以使用musaGetSymbolSize() 函数。
3.2.3. Device Memory L2 Access Management
当内核反复访问全局内存中的数据区域时,可以视为 Persisting accesses; 反过来,如果数据预计只被访问一次,则为 Streaming accesses。为了提高缓存利用率并减少 DRAM 回写,我们可以使数据在 L2 中驻留更长时间,从而提供更高的带宽和对全局内存访问时更低的延迟。
对全局内存的数据访问可分为 3 类:
- 流式访问 (Streaming accesses):数据仅访问一次,因此 cache lines 不太可能保留在 L2 中。
- 持久访问 (Persisting accesses):重复访问数据,因此 cache lines 更有可能持久保持在 L2 中。
- 正常访问 (Normal access):正常访问。
根据访问的类别,我们可以获得缓存替换策略的提示,以实现更好的缓存使用。例如,我们可以用 Persisting accesses 替换 Streaming accesses,因为我们知道后者很可能会在不久的将来缓存命中。
许多 AI 或通用计算工作负载具有可以频繁访问的共享数据,并且本地内存不够大,无法容纳它们。例如,50 层 ResNet 网络有 2600 万个权重参数,并在正向传递中计算 1600 万次激活。内存访问量相当大,因此 L2 缓存效率非常低。如果我们能够正确控制 L2 驻留,可以减少全局内存访问时的延迟,带来一定的性能提升。MUSA 提供了一套 L2 set-aside APIs 来进行驻留和替换控制。
3.2.3.1. L2 cache Set-Aside for Persisting Accesses
可以留出一部分L2高速缓存以用于持久存储对全局存储器的数据访问,而且相比于非持久数据访问,持久性数据访问在使用L2高速缓冲时具有更高的优先权。为持久化数据保留的 LLC 大小可以在硬件限制基础上进行调整。大小需要在程序执行开始前通过musa api进行设置。
musaGetDeviceProperties(&prop, device_id);
size_t size = min(int(prop.l2CacheSize * 0.75), prop.persistingL2CacheMaxSize);
musaDeviceSetLimit(musaLimitPersistingL2CacheSize, size); /* set-aside 3/4 of L2 cache for persisting accesses or the max allowed*/
3.2.3.2. L2 Policy for Persisting Accesses
访问策略窗口(access policy window)指定了全局存储器的连续区域以及用于访问该区域的L2高速缓存中的持久性属性。开发者需要指定access policy window 相关字段的属性:
musaStreamAttrValue stream_attribute; // Stream level attributes data structure
stream_attribute.accessPolicyWindow.base_ptr = reinterpret_cast<void*>(ptr); // Global Memory data pointer
stream_attribute.accessPolicyWindow.num_bytes = num_bytes; // Number of bytes for persistence access.
// (Must be less than musaDeviceProp::accessPolicyMaxWindowSize)
stream_attribute.accessPolicyWindow.hitRatio = 0.6; // Hint for cache hit ratio
stream_attribute.accessPolicyWindow.hitProp = musaAccessPropertyPersisting; // Type of access property on cache hit
stream_attribute.accessPolicyWindow.missProp = musaAccessPropertyStreaming; // Type of access property on cache miss.//Set the attributes to a MUSA stream of type musaStream_t
musaStreamSetAttribute(stream, musaStreamAttributeAccessPolicyWindow, &stream_attribute);
一般在设置访问策略窗口时,会在全局内存中指定一块区域,这里假设A,然后使用 hitRatio 指定这块区域中的持久性访问的比例作为 hitProp 区域,但是 L2 set-aside cache size 是有限的,最终作为 hitProp 区域的是 min(A*hitPrtio, L2 set-aside cache size)。
计算 access policy window 起止地址的方法:
if hit hint Ratio is 1.0:
start = the base VA from base_ptr
end = start + num_bytes
else:
start = the base VA from base_ptr
end = start + num_bytes * Ratio
// Line 5 is a basic implementation.
// A better solution: start = a random value between
// base_ptr and (base_ptr + num_bytes - num_bytes * Ratio)
此外如果两个 kernel 共同访问 L2 set-aside cache,如果两者设置的 size 相交不超过 L2 set-aside cache size,那么没有问题,如果超过,那么会出现冲突。
3.2.3.3. L2 Access Properties
不同的全局内存数据访问定义了三种类型的访问属性:
(1)musaAccessPropertyStreaming:使用流属性进行的内存访问不太可能保留在 L2 高速缓存中,因为这些访问被优先驱逐。
(2)musaAccessPropertyPersisting:由于具有持久属性而发生的内存访问更可能保留在 L2 高速缓存中,因为这些访问优先保留在 L2 高速缓存的预留部分中。
(3)musaAccessPropertyNormal:此访问属性将以前应用的持久访问属性强制重置为正常状态。 先前的MUSA内核具有持久属性的内存访问可能在其预期用途后很长时间被保留在 L2 缓存中。 这种使用后的持久性减少了不使用持久性的后续内核可使用的 L2 缓存数量。 使用 musaAccessPropertyNormal 属性重置访问属性窗口会删除先前访问的持久(优先保留)状态,就好像先前访问没有访问属性一样。
3.2.3.4. L2 Persistence Example
下面是一个例子:
musaStream_t stream;
musaStreamCreate(&stream); // Create MUSA stream
musaDeviceProp prop; // MUSA device properties variable
musaGetDeviceProperties( &prop, device_id); // Query GPU properties
size_t size = min( int(prop.l2CacheSize * 0.75) , prop.persistingL2CacheMaxSize );
musaDeviceSetLimit( musaLimitPersistingL2CacheSize, size); // set-aside 3/4 of L2 cache for persisting accesses or the max allowed
size_t window_size = min(prop.accessPolicyMaxWindowSize, num_bytes); // Select minimum of user defined num_bytes and max window size.
musaStreamAttrValue stream_attribute; // Stream level attributes data structure
stream_attribute.accessPolicyWindow.base_ptr = reinterpret_cast<void*>(data1); // Global Memory data pointer
stream_attribute.accessPolicyWindow.num_bytes = window_size; // Number of bytes for persistence access
stream_attribute.accessPolicyWindow.hitRatio = 0.6; // Hint for cache hit ratio
stream_attribute.accessPolicyWindow.hitProp = musaAccessPropertyPersisting; // Persistence Property
stream_attribute.accessPolicyWindow.missProp = musaAccessPropertyStreaming; // Type of access property on cache miss
musaStreamSetAttribute(stream, musaStreamAttributeAccessPolicyWindow, &stream_attribute); // Set the attributes to a MUSA Stream
for(int i = 0; i < 10; i++) {
musa_kernelA<<<grid_size,block_size,0,stream>>>(data1); // This data1 is used by a kernel multiple times
} // [data1 + num_bytes) benefits from L2 persistence
musa_kernelB<<<grid_size,block_size,0,stream>>>(data1); // A different kernel in the same stream can also benefit
// from the persistence of data1
stream_attribute.accessPolicyWindow.num_bytes = 0; // Setting the window size to 0 disable it
musaStreamSetAttribute(stream, musaStreamAttributeAccessPolicyWindow, &stream_attribute); // Overwrite the access policy attribute to a MUSA Stream
musaCtxResetPersistingL2Cache(); // Remove any persistent lines in L2
musa_kernelC<<<grid_size,block_size,0,stream>>>(data2);
3.2.3.5. Reset L2 Access to Normal
来自先前 MUSA 内核的持久化 L2 高速缓存行可能在使用后很长一段时间内一直保持在 L2 中。因此,将 L2 高速缓存重置为正常对于使用具有正常优先级的 L2 高速缓存的流式传输或正常存储器访问是重要的。有三种方法可以将持久访问重置为正常状态。
- 使用访问属性
musaAccessPropertyNormal重置以前的持久内存区域。 - 通过调用
musaCtxResetPersistingL2Cache()将所有持久化的二级缓存行重置为正常。 - 最终,没有使用的的 cache line 将自动重置为正常,强烈建议不要依赖自动复位,因为自动复位所需的时间长度不确定。
3.2.3.6. Manage Utilization of L2 set-aside cache
在不同流中的核函数有可能同时使用访问策略窗口(access policy window),但是 L2 set-aside cache 是全局的,被共同使用,当使用的资源超过 L2 set-aside cache size 是,会出现性能下降,所以在使用之前要考虑如下事情:
- L2 预留缓存的大小。
- 可以同时执行的 MUSA 内核。
- 可能同时执行的所有 MUSA 内核的访问策略窗口。
- 需要何时以及如何重置 L2 才能允许普通访问或流访问使用具有相同优先级的先前预留的 L2 缓存。
3.2.3.7. Query L2 cache Properties
与L2缓存相关的属性是 musaDeviceProp 结构的一部分,可以使用 MUSA 运行时 API musaGetDeviceProperties 查询。
MUSA设备属性包括:
- l2CacheSize:GPU 上的可用 L2 缓存大小。
- persistenceingL2CacheMaxSize:可以为持久内存访问而保留的 L2 高速缓存的最大数量。
- accessPolicyMaxWindowSize:访问策略窗口的最大尺寸。