博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
[MetalKit]46-Introduction to compute using Metal 用 Metal 进行计算的简介
阅读量:6374 次
发布时间:2019-06-23

本文共 4306 字,大约阅读时间需要 14 分钟。

本系列文章是对 上面MetalKit内容的全面翻译和学习.


在 GPU 编程的领域中,计算或者说GPGPU,是 GPU 编程中除渲染外的另一种用途。它们都涉及到了 GPU 并行编程,不同之处在于在计算中对线程的工作方式进行了更精细的控制。这样,当你想要某些线程来处理问题的某一部分,同时其他线程去处理该问题的另一部分时,就会很有用。

本文是一系列关于计算的文章的开始篇。本文中的主题是关于图像处理,因为它是引入计算和线程管理的最简单方法。

注意:本文假设您知道如何创建一个微型的Metal项目或playground,可以将屏幕清除为纯色。

第一个不同点就是,你需要创建一个MTLComputePipelineState以取代以前渲染时用的MTLRenderPipelineState

let function = library.makeFunction(name: "compute")let pipelineState = device.makeComputePipelineState(function: function)复制代码

第二件事是,你需要一个纹理,以供线程使用。如果你使用的是playground,那你只需要下面几行:

let textureLoader = MTKTextureLoader(device: device)let url = Bundle.main.url(forResource: "nature", withExtension: "jpg")!let image = try textureLoader.newTexture(URL: url, options: [:])复制代码

第三件事,你需要一个MTLComputeCommandEncoder对象,以便将先前创建的管线状态对象和纹理,都附着上去:

commandEncoder.setComputePipelineState(pipelineState)commandEncoder.setTexture(image, index: 0)复制代码

第四件事,你需要一个kernel shader内核着色器,要记得,你之前开始时就为其创建了一个名为compute的函数。当然,你可以将内核代码放到 .metal文件里:

kernel void compute(texture2d
input [[texture(0)]], texture2d
output [[texture(1)]], uint2 id [[thread_position_in_grid]]) { float4 color = input.read(id); output.write(color, id);}复制代码

在着色代码中,输入是你先前创建的MTLTexture对象,称为image输出是一个可绘制纹理,你将向其中写入数据,然后就可以被呈现到屏幕上了:

let drawable = view.currentDrawablecommandEncoder.setTexture(drawable.texture, index: 1)复制代码

第五件事也是最后一件事是,你需要调度线程来干活。有趣的事情就从现在开始了!你需要做的是在commandEncoder中结束编码之前,加上几句代码:

let threadsPerGroup = MTLSizeMake(100, 10, 1)let groupsPerGrid = MTLSizeMake(15, 90, 1)commandEncoder.dispatchThreadgroups(groupsPerGrid, threadsPerThreadgroup: threadsPerGroup)复制代码

那么这里是怎么做的呢?线程是以网格(grid)形式来调度处理数据的,网格可以是 1-,2-,或3-维的。在本例中,你用的是 2D 的网格,因为要处理的是一张图片。不考虑维度的话,网格总是分割成多个线程组的,如下面的公式:

gridSize = groupsPerGrid * threadsPerGroup复制代码

在本例中,你定义一个组有100 x10个线程,每个网格有15 x 90组。如果你运行你的 playground,你会看到类似下面的情况:

边上的红色是什么东西?这是因为你试图去猜测图片的尺寸大小而导致的问题,线程数和组数应该用更“聪明”的方式获取。

显然,图像在两个维度上都大于分派的线程数。您可以做的一件事是使用图像大小进行有根据的猜测,以获得真正应该使用的组数量:

let width = Int(view.drawableSize.width)let height = Int(view.drawableSize.height)let w = threadsPerGroup.widthlet h = threadsPerGroup.heightlet groupsPerGrid = MTLSizeMake(width / w, height / h, 1)复制代码

运行一下,图片看起来会好很多了:

这里又出现一个新的问题---利用不足。请看下图的图表:

通常,您会认为正确设计的网格是3 x 2组,每组4 x 4个线程,因此网格为12 x 8个线程。然而,底部和右侧边缘的一些螺纹未得到充分利用,因为它们没有工作要做。

如果你制作一个较小的网格,比如8 x 4,它将会填满整个组,又会产生你在开始时看到的红色条带。这意味着唯一可接受的解决方案是修复未充分利用问题。您可以通过在每个维度中添加额外的组来解决此问题,如下所示:

let groupsPerGrid = MTLSizeMake((width + w - 1) / w, (height + h - 1) / h, 1) 复制代码

你所做的就是用(w-1, h-1, 1)来实际扩大网格尺寸。这又带来了另一个风险 --- 访问越界坐标。要处理这个问题,您需要在读取输入图像之前向内核着色器添加边界检查:

if (id.x >= output.get_width() || id.y >= output.get_height()) {    return;}复制代码

这将处理那些不应该做任何工作的线程,并处理越界的访问。

那个线程组的大小怎么样 --- 无法优化吗?到目前为止,你一直在猜这些尺寸。当然,还有一种方法可以获得最佳的群组尺寸。硬件提供了一些可以通过管道状态对象(pipeline state object)访问的功能:

var w = pipelineState.threadExecutionWidthvar h = pipelineState.maxTotalThreadsPerThreadgroup / wlet threadsPerGroup = MTLSizeMake(w, h, 1)复制代码

线程执行宽度(在其他API中也称为wavefrontwarp)是GPU组合在一起的线程数,因此它们可以并行地在不同的数据上执行相同的指令。组中的线程数应该是threadExecutionWidth的倍数,但绝不能大于maxTotalThreadsPerThreadgroup

那太棒了!如何找到办法,来避免做这些未充分利用和边界检查呢?Metal 也在这里给你提供了帮助。 无需使用dispatchThreadgroups(),API提供了更新的dispatchThreads()函数,它实现了两件事:

  1. 通过自动创建非均匀线程组(例如3 x 4)来适应边缘情况,这样就避免让你处理未充分利用的问题。
  2. 它甚至可以决定需要多少组,前提是您为其提供网格大小和您想要使用的组大小。

注意:dispatchThreads()函数适用于所有macOS设备,但它不适用于使用A10或更旧处理器的iOS设备。

你需要做的就是,就下面代码替换计算每个网格组数的代码:

w = Int(view.drawableSize.width)h = Int(view.drawableSize.height)let threadsPerGrid = MTLSizeMake(w, h, 1)commandEncoder.dispatchThreads(threadsPerGrid, threadsPerThreadgroup: threadsPerGroup)复制代码

但是等一下,我是不是说过:这里是最好玩的地方?是的,然后来到 kernel shader 中,移除边界检查代码,因为现在已经不需要它了。然后在最后一行前,添加下面代码,倒转颜色通道:

color = float4(color.g, color.b, color.r, 1.0);复制代码

运行一下 playground,你会看到类似下面的图像:

将上一行用下面代码替换,它将灰度应用于图像:

color.xyz = (color.r * 0.3 + color.g * 0.6 + color.b * 0.1) * 1.5;复制代码

运行一下 playground,你会看到类似下面的图像:

最后,将下面代码替换:

float4 color = input.read(id);color.xyz = (color.r * 0.3 + color.g * 0.6 + color.b * 0.1) * 1.5;复制代码

替换为下面的代码,这里将图片将图像像素化为5像素的正方形:

uint2 index = uint2((id.x / 5) * 5, (id.y / 5) * 5);float4 color = input.read(index);复制代码

运行一下 playground,你会看到类似下面的图像:

玩得开心么?希望你玩得开心。如果你想要学习更多关于图像处理的知识,Simon Gladman有一本好书,。本文只是一个对 GPGPU 和GPU计算功能的简短介绍。请继续关注新主题。

已经发布在Github上。本文基于书籍的第 16 章完成。

下次见!

转载于:https://juejin.im/post/5c723f9cf265da2d8a55b7be

你可能感兴趣的文章
Ubuntu Server 上安装 Jexus
查看>>
二台inux主机之间scp复制文件
查看>>
Android studio 申请签名,设置签名key位置 查看 sha1
查看>>
sshpass工具
查看>>
浏览器渲染原理及解剖浏览器内部工作原理
查看>>
IMP数据到指定的表空间
查看>>
向大院大所要智慧——江苏创新转型扫描
查看>>
dubbo连接zookeeper注册中心因为断网导致线程无限等待问题【转】
查看>>
Spring Boot项目配置RabbitMQ集群
查看>>
版本发布后软件测试人员要做的工作
查看>>
android 壁纸设置分析
查看>>
Mysql一些重要配置参数的学习与整理(三)
查看>>
Dockerfile构建Nginx1.14环境
查看>>
bash 交互与非交互
查看>>
介绍Kubernetes监控Heapster
查看>>
linux的free、ps、netstat、tcpdump命令工具介绍
查看>>
javascript学习笔记:DOM节点关系和操作
查看>>
分布式事务
查看>>
怎么提高自身技术
查看>>
HTML5简介,C/S与B/S架构
查看>>