在GPU上执行计算

本文是在苹果文档中学习总结的

使用Metal查找GPU并对其执行计算。

将介绍所有Metal应用程序中使用的基本任务:

将用C编写的简单函数转换为金属着色语言(MSL),以便它可以在GPU上运行

1、寻找GPU

2、通过创建管道准备MSL函数以在GPU上运行

3、创建GPU可访问的内存分配以保存数据

4、创建命令缓冲区并编码GPU命令以操纵数据

5、将缓冲区提交到命令队列以使GPU执行编码命令

编写GPU函数以执行计算

为了说明GPU编程,这个应用程序将两个数组的相应元素添加到一起,将结果写入第三个数组。清单1显示了一个在CPU上执行此计算的函数,用C语言编写。它循环遍历索引,计算循环的每次迭代一个值。

清单1数组添加,用C编写

void add_arrays(const float* inA,
                const float* inB,
                float* result,
                int length)
{
    for (int index = 0; index < length ; index++)
    {
        result[index] = inA[index] + inB[index];
    }
}

const ****float *** p是指p是常量指针,指向float常量或者变量,但是可以改变指向;float *** **const **p 代表p可以指向变量,但是声明指定后就不能再改变

每个值都是独立计算的,因此可以同时安全地计算这些值。要在GPU上执行计算,需要在金属着色语言(MSL)中重写此功能。MSL是为GPU编程而设计的C ++的变体。在Metal中,在GPU上运行的代码称为着色器,因为历史上它们首先用于计算3D图形中的颜色。清单2显示了MSL中的着色器,它执行与清单1相同的计算。项目在文件中定义了此函数。Xcode构建应用程序目标中的所有文件,并创建一个默认的Metal库,它嵌入到您的应用程序中。将在本示例的后面看到如何加载默认库。add.metal.metal

清单2以MSL编写的数组添加

kernel void add_arrays(device const float* inA,
                       device const float* inB,
                       device float* result,
                       uint index [[thread_position_in_grid]])
{
    // the for-loop is replaced with a collection of threads, each of which
    // calls this function.
    result[index] = inA[index] + inB[index];
}

清单1和清单2类似,但MSL版本有一些重要的区别。仔细看看清单2。

首先,该函数添加kernel关键字,声明该函数是:

一个公共GPU功能。公共函数是您的应用可以看到的唯一功能。其他着色器函数也无法调用公共函数。

compute函数(也称为计算内核),其执行使用线程的网格中的并行计算。

请参阅使用渲染管道渲染基元以了解用于声明公共图形函数的其他函数关键字。

该函数使用关键字声明其三个参数,该关键字表示这些指针位于地址空间中。MSL为内存定义了几个不相交的地址空间。每当在MSL中声明指针时,都必须提供一个关键字来声明其地址空间。使用地址空间声明GPU可以读取和写入的持久内存。add_arraysdevicedevicedevice

清单2从清单1中删除了for循环,因为该函数现在将由计算网格中的多个线程调用。此示例创建一个完全匹配数组维度的一维线程网格,以便数组中的每个条目由不同的线程计算。

要替换先前由for循环提供的索引,该函数将index使用另一个MSL关键字获取一个新参数,该参数使用C ++属性语法指定。此关键字声明Metal应为每个线程计算唯一索引,并在此参数中传递该索引。因为使用1D网格,索引被定义为标量整数。即使删除了循环,清单1和清单2也使用相同的代码行将两个数字相加。如果要将类似的代码从C或C ++转换为MSL,请以相同的方式用网格替换循环逻辑。thread_position_in_gridadd_arrays

找一个GPU

在您的应用中,MTLDevice对象是GPU的精简抽象; 你用它来与GPU通信。Metal MTLDevice为每个GPU 创建一个。您可以通过调用获取默认设备对象 。在macOS中,Mac可以有多个GPU,Metal选择其中一个GPU作为默认值并返回该GPU的设备对象。在macOS中,Metal提供了可用于检索所有设备对象的其他API,但此示例仅使用默认值。MTLCreateSystemDefaultDevice

 id<MTLDevice> device = MTLCreateSystemDefaultDevice();

初始化metal物体

Metal将其他与GPU相关的实体(如编译着色器,内存缓冲区和纹理)表示为对象。要创建这些特定于GPU的对象,可以在a上调用方法,MTLDevice或者在由a创建的对象上调用方法MTLDevice。由设备对象直接或间接创建的所有对象仅可用于该设备对象。使用多个GPU的应用程序将使用多个设备对象,并为每个设备创建类似的Metal对象层次结构。

示例应用程序使用自定义类来管理与GPU通信所需的对象。类的初始化程序创建这些对象并将它们存储在其属性中。该应用程序创建此类的实例,传入Metal设备对象以用于创建辅助对象。该对象保持对Metal对象的强引用,直到它完成执行。MetalAdderMetalAdder

MetalAdder* adder = [[MetalAdder alloc] initWithDevice:device];

在Metal中,昂贵的初始化任务可以运行一次,结果保留并且使用成本低廉。您很少需要在性能敏感的代码中运行此类任务。

获取金属功能的参考

初始化程序所做的第一件事是加载函数并准备它在GPU上运行。在构建应用程序时,Xcode会编译该函数并将其添加到它嵌入应用程序的默认Metal库中。您可以使用和对象获取有关Metal库及其中​​包含的函数的信息。要获取表示函数的对象,请要求为默认库创建对象,然后向库请求表示着色器函数的对象。add_arraysMTLLibraryMTLFunctionadd

- (instancetype) initWithDevice: (id<MTLDevice>) device
{
    self = [super init];
    if (self)
    {
        _mDevice = device;
        
        NSError* error = nil;
  
        //在项目中加载具有.metal文件扩展名的明暗器文件

        id<MTLLibrary> defaultLibrary = [_mDevice newDefaultLibrary];
        if (defaultLibrary == nil)
        {
            NSLog(@"Failed to find the default library.");
            return nil;
        }

        id<MTLFunction> addFunction = [defaultLibrary newFunctionWithName:@"add_arrays"];
        if (addFunction == nil)
        {
            NSLog(@"Failed to find the adder function.");
            return nil;
        }
        
        // 创建计算管道状态对象。
        _mAddFunctionPSO = [_mDevice newComputePipelineStateWithFunction: addFunction error:&error];
        if (_mAddFunctionPSO == nil)
        {
            // 如果启用了金属API验证,您可以了解有关about what
            //出了问题。(运行调试生成时,默认情况下启用金属API验证is run
            // 来自xcode)
            NSLog(@"Failed to created pipeline state object, error %@.", error);
            return nil;
        }
        
        _mCommandQueue = [_mDevice newCommandQueue];
        if (_mCommandQueue == nil)
        {
            NSLog(@"Failed to find the command queue.");
            return nil;
        }
    }
    
    return self;
}

准备金属管道

函数对象是MSL函数的代理,但它不是可执行代码。您可以通过创建管道将该函数转换为可执行代码。管道指定GPU执行以完成特定任务的步骤。在Metal中,管道由管道状态对象表示。由于此示例使用计算功能,因此应用程序会创建一个对象。MTLComputePipelineState

_mAddFunctionPSO = [_mDevice newComputePipelineStateWithFunction: addFunction error:&error];

计算管道运行单个计算功能,可选地在运行函数之前操纵输入数据,然后运行输出数据。

创建管道状态对象时,设备对象将完成为此特定GPU编译的功能。此示例同步创建管道状态对象并将其直接返回给应用程序。因为编译确实需要一段时间,所以避免在性能敏感的代码中同步创建管道状态对象。

注意
Metal到目前为止您看到的代码中返回的所有对象都将作为符合协议的对象返回。Metal使用协议来定义大多数特定于GPU的对象,以抽象掉底层实现类,这些类可能因不同的GPU而异。Metal使用类定义与GPU无关的对象。任何给定的Metal协议的参考文档都清楚地表明您是否可以在应用程序中实现该协议。

创建一个命令队列

要将工作发送到GPU,您需要一个命令队列。Metal使用命令队列来安排命令。通过询问一个MTLDevice来创建一个命令队列。

_mCommandQueue = [_mDevice newCommandQueue];

创建数据缓冲区和加载数据

初始化基本Metal对象后,加载要执行的GPU数据。此任务的性能不太重要,但在应用程序启动的早期仍然有用。

GPU可以拥有自己的专用内存,也可以与操作系统共享内存。Metal和操作系统内核需要执行额外的工作,以便将数据存储在内存中并使数据可供GPU使用。Metal使用资源对象抽象此内存管理。(MTLResource)。资源是GPU在运行命令时可以访问的内存分配。使用a MTLDevice为其GPU创建资源。

示例应用程序创建三个缓冲区,并使用随机数据填充前两个缓冲区。第三个缓冲区将存储其结果。add_arrays

- (void) prepareData
{
   // 分配三个缓冲区来保存初始数据和结果。
    _mBufferA = [_mDevice newBufferWithLength:bufferSize options:MTLResourceStorageModeShared];
    _mBufferB = [_mDevice newBufferWithLength:bufferSize options:MTLResourceStorageModeShared];
    _mBufferResult = [_mDevice newBufferWithLength:bufferSize options:MTLResourceStorageModeShared];
    
    [self generateRandomFloatData:_mBufferA];
    [self generateRandomFloatData:_mBufferB];
}

此示例中的资源是(MTLBuffer)对象,它们是没有预定义格式的内存分配。Metal将每个缓冲区管理为不透明的字节集合。但是,在着色器中使用缓冲区时指定格式。这意味着您的着色器和您的应用需要就来回传递的任何数据的格式达成一致。

分配缓冲区时,您需要提供存储模式以确定其某些性能特征以及CPU或GPU是否可以访问它。示例应用程序使用共享内存(),CPU和GPU都可以访问它。MTLResourceStorageMode

要使用随机数据填充缓冲区,应用程序将获取指向缓​​冲区内存的指针,并在CPU上将数据写入其中。清单2中的函数将其参数声明为浮点数的数组,因此您以相同的格式提供缓冲区:add_arrays

- (void) generateRandomFloatData: (id<MTLBuffer>) buffer
{
    float* dataPtr = buffer.contents;
    
    for (unsigned long index = 0; index < arrayLength; index++)
    {
        dataPtr[index] = (float)rand()/(float)(RAND_MAX);
    }
}

创建一个命令缓冲区

请求命令队列创建命令缓冲区。

id<MTLCommandBuffer> commandBuffer = [_mCommandQueue commandBuffer];

创建一个命令编码器

要将命令写入命令​​缓冲区,可以使用命令编码器来处理要编码的特定命令类型。此示例创建一个计算命令编码器,用于编码计算传递。计算传递包含执行计算管道的命令列表。每个计算命令都会使GPU创建一个在GPU上执行的线程网格。

id<MTLComputeCommandEncoder> computeEncoder = [commandBuffer computeCommandEncoder];

要对命令进行编码,请在编码器上进行一系列方法调用。某些方法设置状态信息,如管道状态对象(PSO)或要传递给管道的参数。进行这些状态更改后,您将编码命令以执行管道。编码器将所有状态更改和命令参数写入命令缓冲区。
image

设置管道状态和参数数据

设置要执行命令的管道的管道状态对象。然后为管道需要发送到函数的任何参数设置数据。对于此管道,这意味着提供对三个缓冲区的引用。Metal以参数出现在清单2中的函数声明中的顺序自动为缓冲区参数指定索引,从。开始。您使用相同的索引提供参数。add_arrays0

    [computeEncoder setComputePipelineState:_mAddFunctionPSO];
    [computeEncoder setBuffer:_mBufferA offset:0 atIndex:0];
    [computeEncoder setBuffer:_mBufferB offset:0 atIndex:1];
    [computeEncoder setBuffer:_mBufferResult offset:0 atIndex:2];

您还可以为每个参数指定偏移量。偏移0意味着命令将从缓冲区的开头访问数据。但是,您可以使用一个缓冲区来存储多个参数,为每个参数指定偏移量。
您没有为index参数指定任何数据,因为该函数将其值定义为由GPU提供。add_arrays

指定线程计数和组织

接下来,确定要创建的线程数以及如何组织这些线程。Metal可以创建1D,2D或3D网格。该函数使用一维数组,因此样本创建一个大小为1 x的网格(x 1 x 1),Metal从该网格生成0到-1 之间的索引。add_arraysdataSizedataSize

MTLSize gridSize = MTLSizeMake(arrayLength, 1, 1);

指定线程组大小

Metal将网格细分为称为线程组的较小网格。每个线程组都是单独计算的。Metal可以将线程组分派到GPU上的不同处理元素,以加快处理速度。您还需要确定为命令创建线程组的大小。

 NSUInteger threadGroupSize = _mAddFunctionPSO.maxTotalThreadsPerThreadgroup;
    if (threadGroupSize > arrayLength)
    {
        threadGroupSize = arrayLength;
    }
 MTLSize threadgroupSize = MTLSizeMake(threadGroupSize, 1, 1);

应用程序向管道状态对象请求最大可能的线程组,并在该大小大于数据集大小时收缩它。该属性给出了线程组中允许的最大线程数,这取决于用于创建管道状态对象的函数的复杂性。maxTotalThreadsPerThreadgroup

编写Compute命令以执行线程

最后,编码命令以调度线程网格。

[computeEncoder dispatchThreads:gridSize
          threadsPerThreadgroup:threadgroupSize];

当GPU执行此命令时,它使用您先前设置的状态和命令的参数来分派线程以执行计算。

您可以使用编码器执行相同的步骤,将多个计算命令编码到计算传递中,而不执行任何冗余步骤。例如,您可以设置一次管道状态对象,然后为要处理的每个缓冲区集合设置参数并编码命令。

结束计算通行证

如果没有其他命令要添加到计算传递,则结束编码过程以关闭计算传递。

[computeEncoder endEncoding];

提交命令缓冲区以执行其命令

[commandBuffer commit];

命令队列创建了命令缓冲区,因此提交缓冲区始终将其放在该队列上。提交命令缓冲区后,Metal异步准备执行命令,然后调度命令缓冲区以在GPU上执行。GPU执行命令缓冲区中的所有命令后,Metal将命令缓冲区标记为完成。

等待计算完成

当GPU处理您的命令时,您的应用程序可以执行其他工作。此示例不需要执行任何其他工作,因此只需等待命令缓冲区完成。

[commandBuffer waitUntilCompleted];

或者,要在Metal处理完所有命令时收到通知,请在命令缓冲区()中添加完成处理程序,或通过读取其属性来检查命令缓冲区的状态

从缓冲区读取结果

命令缓冲区完成后,GPU的计算存储在输出缓冲区中,Metal执行任何必要的步骤以确保CPU可以看到它们。在真实的应用程序中,您将从缓冲区读取结果并对其执行某些操作,例如在屏幕上显示结果或将其写入文件。由于计算仅用于说明创建Metal应用程序的过程,因此示例将读取存储在输出缓冲区中的值并进行测试以确保CPU和GPU计算出相同的结果。

- (void) verifyResults
{
    float* a = _mBufferA.contents;
    float* b = _mBufferB.contents;
    float* result = _mBufferResult.contents;

    for (unsigned long index = 0; index < arrayLength; index++)
    {
        assert(result[index] == a[index] + b[index]);
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,470评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,393评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,577评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,176评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,189评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,155评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,041评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,903评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,319评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,539评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,703评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,417评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,013评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,664评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,818评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,711评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,601评论 2 353

推荐阅读更多精彩内容