Metal并行计算以及Metal程序的命令行编译
本来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
Metal并行计算以及Metal程序的命令行编译的更多相关文章
- 使用命令行编译Qt程序
code[class*="language-"], pre[class*="language-"] { color: rgba(51, 51, 51, 1); ...
- C# 控制台程序(命令行程序)设置字体颜色,窗口宽高,光标行数
控制台程序(命令行程序)设置窗口宽度高度,如下代码: Console.WriteLine(Console.WindowHeight); Console.WriteLine(Console.Buffer ...
- 终端命令行编译运行C#程序HelloWorld
终端命令行编译运行C#程序HelloWorld 今天忽然有人问我,刚学习java程序的时候,一般老师都会建议我们从配置环境变量开始从命令行编译运行程序,那么学习c#的时候基本是直接安装visual s ...
- 让.Net程序支持命令行启动
很多时候,我们需要让程序支持命令行启动,这个时候则需要一个命令行解析器,由于.Net BCL并没有内置命令行解析库,因此需要我们自己实现一个.对于简单的参数来说,自己写一个字符串比较函数来分析args ...
- 【linux基础】使用命令行编译运行c++程序
前言 在linux系统运行程序,小鹅知道的有3种编译方式,一种是直接命令行编译,一种是使用Cmake,一种是使用脚本文件(*.sh).本文介绍的是使用命令行编译. 使用过程 注意不同系统的编译器版本可 ...
- 给go程序添加命令行参数
操作系统: CentOS 6.9_x64 go语言版本: 1.8.3 问题描述 需要应用程序根据不同的配置文件访问不同的服务器,希望程序启动时可以指定配置文件. 解决方案 package main i ...
- Win32程序支持命令行参数的做法(转载)
转载:http://www.cnblogs.com/lanzhi/p/6470406.html 转载:http://blog.csdn.net/kelsel/article/details/52759 ...
- Win32程序支持命令行参数的做法
作者:朱金灿 来源:http://blog.csdn.net/clever101 首先说说Win 32 API程序如何支持命令行参数.Win 32程序的入口函数为: int APIENTRY _tWi ...
- 使用命令行编译打包运行自己的MapReduce程序 Hadoop2.6.0
使用命令行编译打包运行自己的MapReduce程序 Hadoop2.6.0 网上的 MapReduce WordCount 教程对于如何编译 WordCount.java 几乎是一笔带过… 而有写到的 ...
随机推荐
- fine-tuning 两阶段模型
目前大部分的nlp任务采用两阶段的模型,第一阶段进行预训练,一般是训练一个语言模型.最出名的是BERT,BERT的预训练阶段包括两个任务,一个是Masked Language Model,还有一个是N ...
- Linux-网络综合实验
题目要求:财务部门有俩台机器acc-1和acc-2,it部门有俩台机器it-2和it-2,要求acc机器互通,it机器互通,财务部和it部机器通过路由器互通
- Win Server 2003 10条小技巧
微软推出Windows Server 2003已经有一段时间了,但是,由于它是一个面向企业用户的服务器操作系统,所以,没有引起更多个人用户的注意.实际上,简单地改变一下系统的设置,您也可以将Windo ...
- PBRT笔记(7)——反射模型
基础术语 表面反射可以分为4大类: diffuse 漫反射 glossy specular 镜面反射高光 perfect specular 完美反射高光 retro-reflective distri ...
- 20190312_浅谈go&java差异(一)
多线程 java java中对于大量的比较耗时的任务多采用多线程对方式对任务进行处理,同时由于进程和线程 本身是通过宿主机OS进行管理的,当在cpu核数较少或线程分配不当 会导致多线程的效果不佳的事常 ...
- dotnet core 3.0 linux 部署小贴士
dotnet core 3.0 目前还是测试版,在linux下安装 sdk 需要有一些注意事项 1.下载url https://dotnet.microsoft.com/download/thank- ...
- centos7安装kubeadm
安装配置docker v1.9.0版本推荐使用docker v1.12, v1.11, v1.13, 17.03也可以使用,再高版本的docker可能无法正常使用. 测试发现17.09无法正常使用,不 ...
- Spring AOP Capabilities ang goals
Spring AOP 是用纯JAVA 实现的. 不需借助JAVA代码在编译处理阶段来实现. Spring 是在运行期实现的.AOP的实现可以在编译,加载,运行三个阶段来实现:Spring AOP 也不 ...
- redis在windows和Linux系统下的下载、安装、配置
1.下载redis安装包 在redis的官网只有Linux系统下的安装包,微软的GitHub上有提供windows版本的redis安装包 redis中文网:http://www.redis.cn/ 微 ...
- python多线程和多进程使用
# 多线程 from concurrent.futures import ThreadPoolExecutor # 多进程 from concurrent.futures import Process ...