大多数浏览器和
Developer App 均支持流媒体播放。
-
MacBook Pro 上的 Metal 计算
探索如何利用最新 MacBook Pro 上的 Metal 计算。了解高性能 Metal 计算的基本原理,以及如何利用这个框架为您的开发流程创造更好的工作流程,甚至为创意专业人士打造更出色的 app。
资源
相关视频
WWDC22
-
下载
Jason Fielder:大家好 我叫 Jason Fielder 来自 Apple 的 GPU 软件与工程团队 我们将了解如何充分利用 全新 M1 Pro 和 M1 Max 笔记本电脑上 出众的图形处理能力 也将探索一些 可供您采用的最佳做法 让您的 app 在我们的 GPU 上 获得出色的性能 最新款 MacBook Pro 配备了我们有史以来 性能最强大的芯片 M1 Pro 具有多达 16 个 GPU 核心 M1 Max 翻了一倍 达到 32 个 同时 DRAM 带宽也显著提高 这让 MacBook Pro 的性能 得到了大幅提升 系统内存现在可供 GPU 使用 这得益于我们的统一内存架构 以及高达 64GB 的可用内存 我们的 GPU app 现在能够访问 比以往更多的内存 这几款 MacBook Pro 将开启一个全新世界 为开发者和创意专业人士 创造各种各样的可能 实现以前只适用于台式计算机的 工作流程 如何开启潜力无限的 GPU 新世界? 我们先回顾一下 Metal 计算 通过这项技术 可将工作调度到 GPU 然后 在了解了 API 和 GPU 架构后 我们将探讨 app 的 各个阶段的最佳做法 最后在总结部分 介绍一些您可以应用的 具体内核优化 首先快速回顾一下 Metal 计算 Metal 是 Apple 推出的 现代低开销 API 用来执行 GPU 工作 它采用尽可能精简、高效的设计 并提供统一的图形和计算接口 Metal 对多线程处理十分友好 支持在多个 CPU 线程之间 轻松地为工作排队 并让您这样的开发者灵活地使用 脱机或联机着色器编译管道 深入探索我们的硬件层 可以发现 CPU 和 GPU 与相同的物理内存连接 CPU 在统一内存块中 创建 GPU 资源 而且 GPU 和 CPU 都能对这些资源进行读写操作 我们要经过几个 API 层 才能看到在 GPU 上执行的内核 最上面一层是命令队列 顾名思义 这个对象让 app 能够 为工作排队以便 某个时候在 GPU 上执行 命令通过命令缓冲区 在 CPU 上批量执行 这些对象是瞬时的 您将创建许多这样的对象 并在对 app 有意义的基础下 决定细化的程度 您可能会发现这是通过 与 CPU 和 GPU 同步 相关的要求来施加的 但简而言之 您需要确保有足够的工作 才能让 GPU 保持满载运行 要将指令放入命令缓冲区 我们需要一个命令编码器 命令编码器有不同的类型 面向不同类型的工作 有适用于 3D 绘图的图形编码器 用于拷贝资源的 位块传送编码器 但本讲座重点关注的是 用于内核调度的 计算编码器 有了计算编码器后 就能对内核调度进行编码 除了内核函数本身外 编码器决定了内核所需的资源 如何与内核绑定在一起 事实上我们可以 将多个内核调度 编码到同一个编码器上 我们可以在各个调度之间 更改内核或所绑定的资源 并告知 Metal 内核调度 是否可以并发执行 还是应该进行序列化 并在上一调度完成后执行 编码完成之后 我们会停止编码器 这会让命令缓冲区可用于 开始编码新的编码器 或将命令缓冲区 提交到 GPU 执行 这里我们向 GPU 编码了 总共 3 个计算编码器 这代表从头至尾的一组工作 现已准备好指示 GPU 开始执行 对 commit 的调用会立即返回 Metal 会确保 在调度工作时 让它等到队列中 前面的所有其他工作 都已完成后再在 GPU 上执行 CPU 线程现已可用于 开始构建新的命令缓存区 或执行适合在 GPU 忙碌时 执行的任何其他 app 工作 但 CPU 可能需要知道 这一组工作在何时完成 从而能够读回其结果 为此我们可以使用 命令缓冲区中的两个技巧 在提交工作之前 app 可以添加一个 CompletionHandler 函数 供 Metal 在工作完成后调用 对于简单情形 我们有一个同步方法 名为 waitUntilComplete 可以阻止发出调用的 CPU 线程 但这里将使用异步方法 这是我们的基本执行模式 API 的最后一个特征是 可以同时编码 多个命令缓冲区 多个 CPU 线程 可以同时编码多个命令缓冲区 并在编码完成后提交工作 如果顺序很重要 可以通过调用 enqueue 在命令队列中 为命令缓冲区预留执行位置 或者也可简单地以所需顺序 调用 commit 可使用最适合 app 的任一方式 由于也能够创建多个命令队列 Metal 允许 app 以最符合需求的方式 将工作灵活地编码到 GPU 以上是对 Metal 呈现的 执行模式的回顾 我们以此为基础 继续探讨如何进行优化 我们准备了这些方面的一些建议 app 应如何访问 GPU 内存 来利用统一内存架构 如何向 GPU 提交工作 以遵循刚才所说的计算模式 以及如何选择哪些资源并按照 最适合 UMA 的方式进行分配 首先要讨论的肯定是 统一内存架构 这一套最佳做法在于最大程度 减少 GPU 所需的工作量 有了统一内存架构后 不需要以传统的方式 在系统 RAM 和视频 RAM 之间 管理副本 Metal 通过共享资源来公开 UMA 使 GPU 和 CPU 能读写相同的内存 随后资源管理就在于同步 CPU 和 GPU 之间的访问 使其在恰当时间安全地进行 而不是在系统内存和视频内存 之间复制或映射数据 从内存中资源的单一实例运行 可以显著降低 app 的 内存带宽需求 并且大幅提升性能 如果存在可能的争用 例如 CPU 需要为下一批工作 更新缓冲区 但 GPU 依然在 执行第一批工作 那就需要显式的多缓冲区模式 CPU 在缓冲区 n 中准备内容 GPU 从缓冲区 n-1 读取内容 然后在下一批次递增 n 这样 app 开发者就能针对 内存开销和访问模式进行调优 并避免不必要的 CPU/GPU 停滞或拷贝 对于 app 所能分配的 GPU 资源数量限制 有两个值需要注意 可以分配的 GPU 资源总量 以及更为重要的 单个命令编码器在任一时间 可以引用的内存量 此限值称为工作集限值 可在运行时通过读取 recommendedMaxWorkingSetSize 从 Metal 设备获取 建议您在 app 中使用这个限值 来帮助控制您希望使用 并确保可用的内存量 虽然单个命令编码器具有 这个工作集限值 但 Metal 能够突破它 分配更多的资源 Metal 为您管理 这些资源的驻留 而且就像系统内存分配一样 在执行 GPU 分配前 也会进行虚拟分配和驻留 通过将资源使用分散到 多个命令编码器 app 就能使用总数 超过工作集大小的资源 并避免与 VRAM 限值 相关的传统约束 这张表格显示了 新款 MacBook Pro 的 GPU 工作集大小 现在对于系统 RAM 为 32GB 的 M1 Pro 或 M1 Max GPU 可以访问 21GB 内存 对于 RAM 为 64GB 的 M1 Max GPU 可以访问 48GB 内存 这是目前为止 Mac 上 可供 GPU 使用的最高内存量 新款 MacBook Pro 产品系列 极大扩充了向用户提供的功能 我们非常期待您能够为用户 赋予各种精彩体验 并体会他们的创造成果 这就是我们在使用 UMA 时 遵循的最佳做法 现在来进入下一个话题 在命令缓冲区层面上 存在与提交相关的延迟 少量的工作可能会导致 更多时间花在等待 而非工作上 尝试将更多编码器分批组合到 一个命令缓冲区中 然后再发出对 commit 的调用 如果 app 花费时间 等待 GPU 结果 告知下一步应调度什么 GPU 时间线上就会出现气泡 在这些气泡中 GPU 会闲置 等待下一调度抵达 要隐藏这个问题 可考虑使用多个 CPU 线程 来处理多份工作 并让 GPU 保持忙碌 可以创建多个命令缓冲区 也可创建多个命令队列 对于内核调度本身 要使 GPU 保持忙碌 可让它处理足够多的线程 并在各个线程包含足够的工作 从而为启动它的开销提供依据 在这里的图像处理示例中 每个像素由一个线程来处理 条件允许的话 您可以在内核调度中 提高线程总数 以确保利用到 GPU 的 所有处理核心 这里使用了一个内核调度 来处理整个图像 使 Metal 和 GPU 在所有可用的 处理核心之间 以最优的方式分配工作 最后 如果您确实 需要较小的线程数 您可以使用并发调度模式 而不是默认的序列化模式 我们发现许多 app 在 M1 上运行不错 但在 M1 Pro 和 M1 Max 上 却显得后劲不足 运用这些技巧 以更大批量提交工作 就能轻松地让您的 app 得以扩展 并充分发挥它的潜能 下一个要讨论的注意事项 是 L1 缓存 Apple 芯片 GPU 含有独立的 L1 缓存 用于纹理读取和缓冲区读取 由于 Metal 是图形和 计算的统一 API 整套纹理对象和采样器 都可供 app 使用 如果您的 app 仅将缓冲区 用于其数据源 则通过将其中一些资源 移到纹理可获得性能效益 这样可以更好地利用 GPU 的高性能缓存 减少 RAM 的流量 并提升性能 我们看看具体是什么样的 在 GPU 访问 RAM 以读取所有资源时 可以使用缓存来改进性能 帮助后续对 相同本地内存区域 进行缓冲区读取 但缓存大小存在限制 而且会很快填满 因此已有一段时间未读取的 旧数据将被丢弃 以便为新读取腾出空间 假设内核处理足够少的数据 那么一旦填充了缓存 所有后续读取都会命中缓存 并且会立即完成 而不会因为要等待 系统内存载入完成 而造成停滞或延迟 通往缓存的带宽明显较高 延迟也比系统 RAM 要低 如果读取未命中缓存 发出调用的线程会停滞 而从 RAM 中获取读取 并放置于缓存中 限制数据读取的是系统内存带宽 而不是芯片上缓存带宽 如果内核从缓冲区访问大量数据 就会以这种方式对缓存造成冲击 并导致性能降低 除了缓冲区缓存外 Apple 芯片 GPU 还包括一个缓存 专门用于纹理读取 app 可以将一些元数据 从 Metal 缓冲区对象 移到 Metal 纹理对象 有效增加缓存空间大小 并提升性能 另外纹理数据是可以扭转的 Metal 会在上传时 自动为您执行这一操作 扭转意味着纹理像素 以更优的顺序排列 实现随机访问模式 并可进一步提升缓存效率 提供比普通缓冲区 更多的性能增益 这在读取时对内核透明 因此不会给内核源增加复杂性 实际上纹理是一直不断的馈赠 Apple 芯片也可以在纹理 创建之后对其进行无损压缩 并在可能时进一步降低 从中读取时的内存带宽 再一次改进了性能 这同样也对着色器内核透明 因为解压缩是在纹理读取 或采样时自动进行的 如果 Metal 纹理 是 GPU 专用的 它会默认进行压缩 但共享和受管的纹理 可以在上传后显式压缩 方法是在位块传输编码器上调用 optimizeContentsForGPUAccess 若要能够使用无损纹理压缩 需要将纹理使用设置为 shaderRead 或 renderTarget 确保在创建纹理对象时 在描述符上进行这一设置 如果纹理数据是实际图像数据 或是以可接受无损压缩的方式 使用的 则可考虑高压缩比有损压缩格式 例如 ASTC 或 BC 这会进一步减少内存占用 和带宽使用量 提升内核的性能 BC 和 ASTC 均可 使用离线工具来生成 可提供出色的图像质量 压缩比为 4:1 到 36:1 不等 我们的工作已进行了优化分批 将缓冲区和纹理用于数据输入 并且能够借助 UMA 减少执行的拷贝工作量 现在可以考虑内核优化了 所有这些最佳做法 均是为了提升内核性能 我们来对其中一些进行探讨 我将重点关注内存索引 全局原子和占用 作为内核中的有待改进之处 同时也会探讨 应在何处使用分析工具 以了解内核的瓶颈 以及如何衡量可能取得的 任何改进和优化 在去年的 WWDC 上 我们的 GPU 软件团队 发布了一段关于 Apple 芯片 Metal 优化技巧的视频 这里简要回顾那个讲座的一些内容 如需完整详情和示例 请一定去观看那个讲座 首先谈谈内存索引 在索引到数组时 请使用带符号整数类型 不要使用无符号类型 这里有一个 for 循环 其计数变量 i 声明为不带符号 由于在着色器语言规范中 uint 具有包装特性 这会停用矢量化载入 这通常不是您想要的 可以使用带符号类型 来避免产生额外代码 这里没有定义 int 的包装行为 其载入将是矢量化的 并有机会提高性能 随着新款 MacBook Pro 中 GPU 核心和内存带宽的增加 我们发现一些 GPU 工作负载的 主要瓶颈从 ALU 或内存带宽使用 转移到了其他方面 其中一个方面是全局原子 我们建议最大程度减少 在内核中使用的原子运算 或者改为使用围绕 线程组原子建立的技巧 对于所有良好的优化工作流 请先对着色器进行分析 以了解这是不是问题所在 因为适度使用原子并无不妥 如何获取这一重要的分析信息? 通过使用 Xcode 中的 GPU Frame Debugger 这是执行此类评测的有效工具 它可提供关于 GPU 上 所执行工作的丰富见解 采集后即可浏览 时间线视图提供 工作负载的全面概要 并通过图表来视觉化显示 主要的 GPU 性能计数器 这些计数器中大多都提供 使用量和限制值 以这里的 ALU 为例 使用量数值告诉我们 内核在执行期间使用了 GPU 约 27% 的 ALU 能力 其他时间花在执行其他任务上 例如数据读取和写入 做出控制逻辑决策等 限制值意味着 在大约 31% 的 内核执行时间里 GPU 存在 ALU 使用瓶颈 为什么 GPU 在使用 GPU 27% 的 ALU 能力 却在 31% 的时间里 存在 ALU 瓶颈? 限制值可以想象成 已完成 ALU 工作的效率 它是执行实际工作所花费的时间 加上因为内部停滞或 低效率而等待的时间 理想状态下这些时间是相等的 但实践中这存在差距 差距较大表示 GPU 有工作要做 但因为某种原因而无法完成 例如 log() 等复杂的 ALU 运算 或使用成本很高的纹理格式 可能会导致利用率不足 并提示内核数学上 存在可以优化的空间 这两个数据相结合 可帮助您了解内核 正在执行的工作的总体构成 以及各类工作的执行效率 对于这一个内核 我们可以看到占用率为 37% 这个值看起来比较低 当然值得调查一下 以了解能不能增大 我们来仔细看看占用率 它衡量的是 GPU 上 目前活跃的线程数 相对于潜在上限的比率 这个值较低时 务必要了解其原因 以判断这是正常现象 还是提示有问题 低占用率有时并不奇怪 而且难免会出现 举个例子 假如您提交的工作 因为要执行的工作量很小 而造成线程数相对较低 如果 GPU 因为 ALU 等 其他计数器而受限 这也没有问题 不过 如果占用率和限制值计数器 这两个数据同时很低 这表明 GPU 还有余地 可同时执行更多的线程 什么原因会导致 有问题的低占用率? 常见原因是线程 或线程组内存被耗尽 这两个资源 在 GPU 上是有限的 并且在运行的线程之间共享 线程内存由寄存器支持 当寄存器压力增加时 可以通过降低占用率来适应 如果线程组内存使用量较高 则提高占用率的唯一途径是 减小使用的共享内存大小 减少线程组内存也有助于 降低线程寄存器压力的影响 如果在管道状态创建时 已知线程组中的最大线程数 则编译器有余地来提高 寄存器溢出效率 若要启用这些优化 可在计算管道状态描述符上设置 maxThreadsPerThreadgroup 或直接在您的内核源中 使用 Metal 着色器语言 max_total_threads_per threadgroup 属性 通过调节这个值来找到 与内核最契合的平衡 目标值是适用于您的算法的 线程执行宽度最小倍数 我们来深入研究一下 寄存器宽度 如果这个值很高 就会在 Xcode 的 GPU 分析器中看到寄存器溢出 在这个内核示例中 我们可以看到 占用率为 16% 这是非常低的 查看此内核的编译器统计 可以看到它的相对指令成本 包括溢出的字节数 此溢出和临时寄存器 可能是占用率较低的原因 我们正在耗尽线程内存 并通过降低占用率 为运行的线程 释放更多寄存器 寄存器在寄存器块中 分配给内核 因此您需要将使用率减小到 不超过块大小的值 以查看是否有机会提高占用率 通过优化尽可能 降低寄存器使用量 是有效改进复杂内核性能 的一种不错方法 但应该怎么做? 选用 16 位类型 而非 32 位类型 可以增加提供给内核其他部分 使用的寄存器数量 在这些类型和 32 位类型之间 转换通常不需要代价 而减少堆栈中存储的数据 例如 大型数组或结构 可消耗大量的寄存器 减少其数量是一种有效的方法 设法调节您的着色器输入 以充分利用常量地址空间 这可大大减少不必使用的 通用寄存器的数量 最后一点提示是 不要通过动态索引 索引到存储在堆栈上 的数组或索引到常量数据 正如此处的例子所示 数组在运行时初始化 如果索引在编译时 对是编译器未知的 数组有可能会从内存溢出 但在第二个示例中 索引在编译时已知 编译器有可能会解开循环 并且能够优化掉任何溢出 这些技巧各自都能减少 寄存器分配和溢出 也有助于提高占用率 来改进内核性能 如需更深入地了解 Apple 芯片的 Metal 优化技巧 请观看 WWDC 2020 视频 “为搭载 Apple 芯片的 Mac 优化 Metal 性能” 今天的内容就是这些 我们来做个总结 首先回顾了命令队列、 命令缓冲区和 命令编码器的作用 以提醒自己注意提交模式 以及 Metal 是如何 在 GPU 中为工作排队的 我们也探讨了如何从多个线程 编码 Metal 命令以减少 CPU 编码时间和成本 掌握了这些知识后 我们探讨了有关 关于如何调优 app 的建议 通过避免不必要的拷贝 来利用统一内存架构 提交更大数量的工作 以及将 Metal 纹理 和 Metal 缓冲区 用于内核资源 最后 我们逐步演示了 如何使用工具来 识别性能瓶颈 我们知道了如何解读 GPU 使用量和限制值 以及在发现 有问题的低占用率时 如何加以处理 非常感谢 希望您和我一样热切期待 史上最强 MacBook Pro 产品系列带来的诸多可能
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。