本来Cuda用的挺好,为了Apple,放弃Cuda,改投OpenCl。好不容易OpenCl也算熟悉了,WWDC2018又宣布了Metal2,建议大家放弃OpenCl,使用Metal Performance Shaders。

Apple是一个富有“革命性”创新力的公司,很多创新,会彻底的放弃原有的积累。不断带来新能力的同时,也让人又爱又恨。

下面是一个例子,用于演示如何使用Metal+Shader来加速mac的大规模数据计算。

主程序使用swift。随机生成一个大规模的整数数组,然后分配到GPU内核上并行对数组进行求和。

Metal部分的各项逻辑建议看官方文档https://developer.apple.com/metal/,只重点说一下计算部分。计算是由Shader子程序(核函数)完成的,Shader编程所使用的语言衍生自c++14,所以跟cpu通讯所使用的数据结构基本都是使用c语言可以接受的类型。可以把Shader语言理解为c++的一个子集。官方的建议是可以有大量的计算,但尽力减少逻辑语句之类需要GPU核进行预判从而降低速度的内容。大多情况下单个内核的计算速度并不快,使用GPU加速计算的原因是GPU都具有很多个计算单元进行并行的计算。

通常在Shader函数的参数中,至少包含3个部分:输入、输出、进程的ID。前两个参数好理解,第三个参数就是因为该核函数可能随机的运行在某个GPU内核上进行计算工作,应当根据这个唯一的ID分配出来唯一的任务在程序中来计算,从而达到并发的效果。所以核函数都应当是支持并发、支持数据切割分块计算。

Metal对并发的支持首先是线程组数量threadgroupsPerGrid,这个基本上是跟GPU核心数相关的,另一个是批次数量threadsPerThreadgroup,这个要求是线程组数量的整倍数。

其它的内容请看代码中的注释。主程序命名为testCompute.swift

import Metal

//定义数据集长度,总共count个数据求和
//swift数字立即数中可以添加下划线表现出来科学计数法的方式,很有特色
let count = 10_000_000
//每elementsPerSum个数据分配到一个核汇总一次
let elementsPerSum = 10_000 //每个数据的类型,必须使用C兼容的类型,
//因为GPU运行的shader语言是从C++14衍生来的
typealias DataType = CInt //设备,就是GPU
let device = MTLCreateSystemDefaultDevice()!
//载入当前目录下的default.metallib(编译后的shader),使用其中的parsum核函数
let parsum = device.makeDefaultLibrary()!.makeFunction(name:"parsum")!
//如果shader文件不是默认名称,可以使用下面的方法载入指定文件
//let lib = try! dev.makeLibrary(filepath:"default.metallib")!.newFunctionWithName("parsum")!
//建立GPU运算的流水线
let pipeline = try! device.makeComputePipelineState(function:parsum)
//生成随机数据集
var data = (0..<count).map{ _ in DataType(arc4random_uniform(100)) }
//传递给核函数的数据总数,所以也用C兼容方式
var dataCount = CUnsignedInt(count)
//传递给核函数的每组汇总数量,同上
var elementsPerSumC = CUnsignedInt(elementsPerSum)
//返回的分批汇总的结果数
let resultsCount = (count + elementsPerSum - 1) / elementsPerSum //建立两个同GPU通信的缓冲区,一个用于输入给核函数,一个用用于核函数返回结果
let dataBuffer = device.makeBuffer(bytes:&data, length: MemoryLayout<CInt>.size * count, options: []) // Our data in a buffer (copied)
let resultsBuffer = device.makeBuffer(length:MemoryLayout<CInt>.size * resultsCount, options: []) // A buffer for individual results (zero initialized)
//返回结果是c指针,要转换成swift可访问的数据类型
let results = UnsafeBufferPointer<DataType>(
start: resultsBuffer!.contents().assumingMemoryBound(to:CInt.self), count: resultsCount)
//建立GPU命令队列
let queue = device.makeCommandQueue()
//GPU命令缓冲区,一般有多个运算会都放置到缓冲区,一次性提交执行
let cmds = queue!.makeCommandBuffer()
//命令编码器是用于将一条GPU核函数调用的函数、参数等打包到一起
let encoder = cmds!.makeComputeCommandEncoder()! //设置一条GPU核函数调用的函数及其相关参数,如前所述,必须使用C兼容的类型
encoder.setComputePipelineState(pipeline)
encoder.setBuffer(dataBuffer, offset: 0, index: 0)
encoder.setBytes(&dataCount, length: MemoryLayout<CUnsignedInt>.size, index: 1)
encoder.setBuffer(resultsBuffer, offset: 0, index: 2)
encoder.setBytes(&elementsPerSumC, length: MemoryLayout<CUnsignedInt>.size, index: 3) //设定每组任务数量
let threadgroupsPerGrid = MTLSize(width: (resultsCount + pipeline.threadExecutionWidth - 1) / pipeline.threadExecutionWidth, height: 1, depth: 1)
//设定每批次任务数量,必须是上面组数量的整倍数
let threadsPerThreadgroup = MTLSize(width: pipeline.threadExecutionWidth, height: 1, depth: 1)
//分配任务线程
encoder.dispatchThreadgroups(threadgroupsPerGrid, threadsPerThreadgroup: threadsPerThreadgroup)
//完成一个调用的所有设置
encoder.endEncoding() var start, end : UInt64
var result : DataType = 0 start = mach_absolute_time()
//真正提交任务
cmds!.commit()
//等待完成GPU计算
cmds!.waitUntilCompleted()
//GPU计算式分批次汇总的,数量已经很少了,最后用CPU进行完整的汇总
for elem in results {
result += elem
}
end = mach_absolute_time()
//显示GPU计算结果及所用时间
print("Metal result: \(result), time: \(Double(end - start) / Double(NSEC_PER_SEC))")
result = 0 //下面是使用CPU完整的计算一次,并显示结果、耗费时间
start = mach_absolute_time()
data.withUnsafeBufferPointer { buffer in
for elem in buffer {
result += elem
}
}
end = mach_absolute_time()
print("CPU result: \(result), time: \(Double(end - start) / Double(NSEC_PER_SEC))")

shade程序命名为:shader.metal

//各项数据类型必须跟Swift中定义的相同
#include <metal_stdlib>
typedef unsigned int uint;
typedef int DataType; kernel void parsum(const device DataType* data [[ buffer(0) ]],
const device uint& dataLength [[ buffer(1) ]],
device DataType* sums [[ buffer(2) ]],
const device uint& elementsPerSum [[ buffer(3) ]], const uint tgPos [[ threadgroup_position_in_grid ]],
const uint tPerTg [[ threads_per_threadgroup ]],
const uint tPos [[ thread_position_in_threadgroup ]]) {
//根据组索引、批次索引、组中位置计算总的索引值,这个是唯一的
uint resultIndex = tgPos * tPerTg + tPos;
//计算本批次数据的开始结束位置
uint dataIndex = resultIndex * elementsPerSum; // Where the summation should begin
uint endIndex = dataIndex + elementsPerSum < dataLength ? dataIndex + elementsPerSum : dataLength; // The index where summation should end
//对本批次数据求和
for (; dataIndex < endIndex; dataIndex++)
sums[resultIndex] += data[dataIndex];
}

给一个在命令行使用的编译脚本:

#!/bin/bash
xcrun metal -o shader.air shader.metal
xcrun metal-ar rcs shader.metal-ar shader.air
xcrun metallib -o default.metallib shader.metal-ar
swiftc testCompute.swift⏎

在我的笔记本上运行效果如下:

metal> ./testCompute
Metal result: 495056208, time: 0.017362745
CPU result: 495056208, time: 1.210801891

作为一个比较片面的比较,GPU计算速度,比CPU快121倍。

测试环境:

MacBook Pro (13-inch, 2017, Four Thunderbolt 3 Ports)

CPU:3.1 GHz Intel Core i5

Graphics:Intel Iris Plus Graphics 650 1536 MB

Memory:8 GB 2133 MHz LPDDR3

Xcode:9.4.1

参考资料:

https://stackoverflow.com/questions/38164634/compute-sum-of-array-values-in-parallel-with-metal-swift

Metal并行计算以及Metal程序的命令行编译的更多相关文章

  1. 使用命令行编译Qt程序

    code[class*="language-"], pre[class*="language-"] { color: rgba(51, 51, 51, 1); ...

  2. C# 控制台程序(命令行程序)设置字体颜色,窗口宽高,光标行数

    控制台程序(命令行程序)设置窗口宽度高度,如下代码: Console.WriteLine(Console.WindowHeight); Console.WriteLine(Console.Buffer ...

  3. 终端命令行编译运行C#程序HelloWorld

    终端命令行编译运行C#程序HelloWorld 今天忽然有人问我,刚学习java程序的时候,一般老师都会建议我们从配置环境变量开始从命令行编译运行程序,那么学习c#的时候基本是直接安装visual s ...

  4. 让.Net程序支持命令行启动

    很多时候,我们需要让程序支持命令行启动,这个时候则需要一个命令行解析器,由于.Net BCL并没有内置命令行解析库,因此需要我们自己实现一个.对于简单的参数来说,自己写一个字符串比较函数来分析args ...

  5. 【linux基础】使用命令行编译运行c++程序

    前言 在linux系统运行程序,小鹅知道的有3种编译方式,一种是直接命令行编译,一种是使用Cmake,一种是使用脚本文件(*.sh).本文介绍的是使用命令行编译. 使用过程 注意不同系统的编译器版本可 ...

  6. 给go程序添加命令行参数

    操作系统: CentOS 6.9_x64 go语言版本: 1.8.3 问题描述 需要应用程序根据不同的配置文件访问不同的服务器,希望程序启动时可以指定配置文件. 解决方案 package main i ...

  7. Win32程序支持命令行参数的做法(转载)

    转载:http://www.cnblogs.com/lanzhi/p/6470406.html 转载:http://blog.csdn.net/kelsel/article/details/52759 ...

  8. Win32程序支持命令行参数的做法

    作者:朱金灿 来源:http://blog.csdn.net/clever101 首先说说Win 32 API程序如何支持命令行参数.Win 32程序的入口函数为: int APIENTRY _tWi ...

  9. 使用命令行编译打包运行自己的MapReduce程序 Hadoop2.6.0

    使用命令行编译打包运行自己的MapReduce程序 Hadoop2.6.0 网上的 MapReduce WordCount 教程对于如何编译 WordCount.java 几乎是一笔带过… 而有写到的 ...

随机推荐

  1. centos7.x 安装 fastDFS

    环境准备 使用的系统软件 名称 说明 centos 7.x libfatscommon FastDFS分离出的一些公用函数包 FastDFS FastDFS本体 fastdfs-nginx-modul ...

  2. SQL Server 优化

    SELECT TOP 10 [Total Cost] = ROUND(avg_total_user_cost * avg_user_impact * (user_seeks + user_scans) ...

  3. 十分钟了解HTTPS

    一.为什么要用HTTPS——HTTP协议的缺陷 通信使用明文(不加密),内容可能会被窃听 不能验证通信方的身份,所以请求和响应都有可能是攻击者发送的 数据包在由A到B的过程中,可能经历很多次路由转发, ...

  4. 在github上面创建新的分支

    第一步:git branch 查看当前分支情况 git branch //查看当前分支情况 第二步:git branch 分支名,新建一个自己的分支 git branch 分支名 // 新建一个自己的 ...

  5. ctf密码学习题总结

    1.变异凯撒 加密密文:afZ_r9VYfScOeO_UL^RWUc格式:flag{ }   一看题中说的是凯撒加密,我就赶快使用工具列出了所有的组合,然而发现没有一个是我想要的. 于是乎,又重新审题 ...

  6. 用Java写hello world

    public class HelloWorld{ public static void main(String[] args){ System.out.println("hello worl ...

  7. MySQL视图更新

    昨天在写美团2019秋招笔试题的时候遇到了关于视图是否能更新的问题,突然感觉这个问题之前复习的时候重点关注过,但是却又想不全.今天特地搜了一些资料总结一下.本文主要说明视图的更新限制,如需关于视图的更 ...

  8. unix的发展

    转载http://blog.51cto.com/1193432/1671058

  9. 关于Java 去除空格,换行的代码

    public static String replaceBlank(String str) { String dest = ""; if (str != null) { //Pat ...

  10. SQL DISTINCT去掉重复的数据统计方法【转】

    SELECT指令让我们能够读取表格中一个或数个栏位的所有资料.这将把所有的资料都抓出,无论资料值有无重复.在资料处理中,我们会经常碰到需要找出表格内的不同资料值的情况.换句话说,我们需要知道这个表格/ ...