【AI系统】计算与调度

news2024/12/26 19:43:55

计算与调度

上一篇文章我们了解了什么是算子,神经网络模型中由大量的算子来组成,但是算子之间是如何执行的?组成算子的算法逻辑跟具体的硬件指令代码之间的调度是如何配合?这些内容将会在本文进行深入介绍。

计算与调度

计算与调度的来源

图像处理在当今物理世界中是十分基础且开销巨大的计算应用。图像处理算法在实践中需要高效的实现,尤其是在功耗受限的移动设备上。随着算法和计算硬件的不断发展和改进,开发者越来越难以编写高效的图像处理代码。这不是使用 C 或 C++ 这类语言进行编程就能解决的问题,原生 C 和高度优化的 C 代码之间的性能差异通常能达到数量级级别。

此外,现代硬件上的高效实现需要计算和数据结构的复杂全局转换,这种优化通常以程序员的痛苦和代码复杂性为代价,因为必须重新组织计算才能有效地利用内存层次结构和并行执行硬件。

这一挑战的根源来自传统的编程语言将图像处理算法的定义及其计算和数据在底层机器上的组织方式混为一谈。这使得编写算法、将它们组合成更大的应用程序、组织它们以在给定机器上高效执行或重新组织它们以在不同架构上高效执行变得困难。

现在以 Halide 为例。Halide 的独特之处在于将算法定义和如何组织计算两个过程独立。其作为一种强大的编程工具,能让图像处理程序变得更简单,但性能却比之前的手动调优方法快很多倍。它的好处在于,无论是什么类型的处理器,都可以轻松地进行调优。而且,它还能让代码更易于组合和修改,这在传统的实现方法中是很难做到的。

Halide 是用 C++ 作为宿主语言的一个图像处理相关的 DSL(Domain Specified Language)语言。主要的作用为在软硬层面上(与算法本身的设计无关)实现对算法的底层加速。

在 Halide 中计算定义了如何生成输出图像像素的方式,可以包括简单的像素级操作、复杂的算法表达式以及各种变换和滤波操作。并且 Halide 提供了丰富的调度器来帮助用户优化他们的计算图,包括并行化、向量化、内存布局优化等技术,使得用户可以更灵活地控制计算的执行方式。Halide 将计算与其实现解耦合,可以更加高效的设计算法的具体执行过程,使得用户可以专注于底层加速。

计算与调度的含义

有一个耳熟能详的高斯得出求和公式的故事,当时,老师让学生计算 1 到 100 的和,本来需要逐个相加,但是高斯很快就找到了一个简便的方法。他观察到,如果将这些数字按照顺序两两配对,比如 1 和 100、2 和 99、3 和 98,以此类推,每对数字的和都是 101。而总共有 50 对这样的数字组合,所以他通过 50 乘以 101 来得到 1 到 100 的和,即 5050。

对于计算机运算来说,也存在这样的”捷径“。一个计算,可以简单的按照最原始的模式一个一个执行,也可以利用各种特殊硬件如专门的存储或者计算组件来加速这个过程。这是一个一对多的映射,这个计算本身可以有多种不同的实现方式,这些实现方式在不同场景、不同输入、不同机器、不同参数上各有千秋,没有一个最佳的覆盖所有面的实现。在这个背景下,分离出了计算和调度两个概念:

  • 计算:描述实现算法的具体逻辑,而不关心具体的代码实现。

  • 调度:对计算进行优化和控制的过程。通过调度,可以指定计算的执行顺序、内存布局、并行策略等以实现对计算性能的优化。

在神经网络中,深度学习算法由一个个计算单元组成,我们称这些计算单元为算子(Operator,简称 Op)。算子是一个函数空间到函数空间上的映射 O : X → Y O:X→Y OXY;从广义上讲,对任何函数进行某一项操作都可以认为是一个算子。于 AI 框架而言,所开发的算子是网络模型中涉及到的计算函数。

在神经网络中矩阵乘法是最常见的算子,矩阵乘法的公式为:

C i j = ∑ k = 1 n A i k ⋅ B k j C_{ij} = \sum_{k=1}^{n} A_{ik} \cdot B_{kj} Cij=k=1nAikBkj

,其最朴实的实现如下代码:

void matrixMultiplication(int A[][128], int B[][128], int result[][128], int size) {
    for (int i = 0; i < size; ++i) {
        for (int j = 0; j < size; ++j) {
            result[i][j] = 0;
            for (int k = 0; k < size; ++k) {
                result[i][j] += A[i][k] * B[k][j];
            }
        }
    }
}

使用循环分块对其进行优化:

void matrixMultiplicationTiled(int A[][128], int B[][128], int result[][128], int size, int tileSize) {
    for (int i = 0; i < size; i += tileSize) {
        for (int j = 0; j < size; j += tileSize) {
            for (int k = 0; k < size; k += tileSize) {
                for (int ii = i; ii < i + tileSize; ++ii) {
                    for (int jj = j; jj < j + tileSize; ++jj) {
                        int sum = 0;
                        for (int kk = k; kk < k + tileSize; ++kk) {
                            sum += A[ii][kk] * B[kk][jj];
                        }
                        result[ii][jj] += sum;
                    }
                }
            }
        }
    }
}

抑或是使用向量化对其优化:

#include <immintrin.h>

void matrixMultiplicationVectorized(int A[][128], int B[][128], int result[][128], int size) {
    for (int i = 0; i < size; ++i) {
        for (int j = 0; j < size; j += 4) {
            __m128i row = _mm_set1_epi32(A[i][j]);
            for (int k = 0; k < size; ++k) {
                __m128i b = _mm_loadu_si128((__m128i*)&B[k][j]);
                __m128i product = _mm_mullo_epi32(row, b);
                __m128i currentResult = _mm_loadu_si128((__m128i*)&result[i][j]);
                __m128i updatedResult = _mm_add_epi32(currentResult, product);
                _mm_storeu_si128((__m128i*)&result[i][j], updatedResult);
            }
        }
    }
}

我们还可以使用更多的优化方式来实现矩阵乘法,或是将它们组合起来。上面三种操作的算法功能是一样的,但是速度是有差异的。这种差异是和硬件设计强相关的,计算机为加快运算做了许多特殊设计,如存储层次、向量加速器、多个核心等,当我们充分这些硬件特性,可以极大地提升程序执行的速度,优化后的运行效率是原始程序效率的几十倍甚至几百倍。

算子调度具体执行的所有可能的调度方式称为调度空间。AI 编译器优化的目的在于通过对算子进行最佳调度,使得算子在特定硬件上的运行时间达到最优水平。这种优化涉及到对算子调度空间的全面搜索和分析,以确定最适合当前硬件架构的最佳调度方案。这样的优化过程旨在最大程度地利用硬件资源,提高算子的执行效率,并最终实现整体计算任务的高性能执行。

调度树基本概念

在构建一个算子的调度空间时,首先要确定我们能使用哪些优化手段。同样以 Halide 为例,可以使用的优化有 Reorder(交换)、Split(拆分)、Fuse(融合)、Tile(平铺)、Vector(向量化)、展开(Unrolling)、并行(Parallelizing)等,以 Halide 思想为指导的 AI 编译器 TVM 继承了这些优化方式:

  • Reorder(交换):重新排列计算的顺序,可以改变计算的依赖关系,有助于提高缓存命中率,降低内存访问延迟,从而提高性能。

  • Split(拆分):将一个维度的计算拆分成多个较小的维度,可以帮助并行化和向量化,并优化内存访问模式。

  • Fuse(融合):合并多个计算,减少内存访问和数据传输的开销,提高计算的局部性,以及减少调度开销。

  • Tile(平铺):将计算划分成小的块,有利于并行化和向量化,并且可以提高缓存的命中率。

  • Vector(向量化):通过将多个数据元素打包成矢量操作,充分利用 SIMD 指令集,提高计算密集型任务的效率。

  • 展开(Unrolling):循环展开,减少循环的开销,减少分支预测失败的可能性,提高指令级并行性。

  • 并行(Parallelizing):将计算任务分解成多个并行执行的子任务,充分利用多核处理器或者并行处理单元,提高整体计算速度。

对于神经网络中的算子来说,其计算形式一般比较规则,是多层嵌套的循环,也很少有复杂的控制流,并且输入主要是多维张量。分析完计算的特点后,我们来分析下调度的要素。对于一个计算,其首先要进行存储的分配以容纳输入,之后在多层循环下进行计算,得出最终结果后再存储回结果位置。

// in 为输入原始图像 blury 为输出模糊后的图像
void box_filter_3x3(const Mat &in, Mat &blury)
{
    Mat blurx(in.size(), in.type());  // 存储

    for(int x = 1; x < in.cols-1; x ++)
        for(int y = 0 ; y < in.rows; y ++)   //循环
            blurx.at<uint8_t >(y, x) = static_cast<uint8_t>(
                    (in.at<uint8_t >(y, x-1) + in.at<uint8_t >(y, x) + in.at<uint8_t >(y, x+1)) / 3);  //计算

    for(int x = 0; x < in.cols; x ++)
        for(int y = 1 ; y < in.rows-1; y ++) //循环
            blury.at<uint8_t >(y, x) = static_cast<uint8_t>(
                    (blurx.at<uint8_t >(y-1, x) + blurx.at<uint8_t >(y, x) + blurx.at<uint8_t >(y+1, x)) / 3);  //计算
}

根据调度的要素,可以将其抽象为一个树结构,称为调度树:

  • 循环节点:表示函数如何沿着给定维度进行遍历计算。循环节点与一个函数和一个变量(维度)相关联。循环节点还包含循环是按顺序运行、并行运行还是矢量化运行等信息。

  • 存储节点:表示存储待使用的中间结果。

  • 计算节点:调度树的叶子,表示正在执行的计算。计算节点可以有其他计算节点作为子节点,以表示内联函数而不是从中间存储加载。

调度树需要满足几个约束才能使调度合法:

  • 函数必须在使用之前进行计算:在调度树的深度优先遍历中,函数的计算节点必须出现在其调用函数的计算节点之前。

  • 存储必须已分配并在要使用的作用域内:函数的存储节点必须是其计算节点及其调用者的计算节点的祖先。

  • 实际代码生成的限制使得某些模式非法。特别是,我们只允许最内层循环(不是任何其他循环节点的祖先的循环节点)的矢量化,并且只允许确定宽度循环的矢量化和展开。

对于任意的算子,可以定义其默认调度。其以行主序的形式遍历所有输出,并且内联所有函数调用,如下图所示:

在这里插入图片描述

我们将调度树与原有的程序进行对应:

在这里插入图片描述

在给定一个调度树后,可以通过深度优先搜索的方式进行遍历,然后转换成对应的程序代码:

  • 如果它是一个循环节点,它将使用适当的属性(并行、矢量化、展开)开始相应的循环,按顺序递归访问每个子节点,并结束相应的循环。

  • 如果是存储节点,则分配相应的存储,递归访问每个子节点,并释放存储。

  • 如果它是计算节点,则它在其循环定义的域中的位置处计算相应函数的值,并将该值存储在其分配的存储中。

这里就体现计算与调度分离的好处,对于一个计算,可以有多个调度树生成不同性能的程序,只要调度树是合法的,就可以在结果正确的前提下提升程序的性能。

调度变换的方式

Halide 调度变换

在调度中可以使用许多优化手段,这些方式可以通过变换调度树来体现。当然在代码中 Halide 提供了封装好的 api,原始代码:

Var x("x"), y("y"); //定义两个变量
Func gradient("gradient");  //定义一个待执行的 function
gradient(x, y) = x + y;
// realize 即为实现这个操作到了这一步才会对上述的操作进行编译并执行
Buffer<int> output = gradient.realize(4, 4);

这个代码转换为 C++ 就是:

for (int y = 0; y < 4; y++) {
    for (int x = 0; x < 4; x++) {
        printf("Evaluating at x = %d, y = %d: %d\n", x, y, x + y);
    }
}

接下来使用一些调度提供的变换来进行优化。例如对于 Fuse:

Var fused;
gradient.fuse(x, y, fused);

//对应的 C++代码
for (int fused = 0; fused < 4*4; fused++) {
    int y = fused / 4;
    int x = fused % 4;
    printf("Evaluating at x = %d, y = %d: %d\n", x, y, x + y);
}

在调度树中,它就进行这样的变换:将树中同一函数的两个相邻循环节点合并为一个循环节点,新节点与原始外部循环节点保持在树中的相同位置,并且每个节点的子节点都连接起来,原始外部变量的子节点位于原始内部变量的子节点之前。

在这里插入图片描述

在这一步首先对 x 轴和 y 轴进行循环分块,分块因子为 4,然后将外侧的 y 和外侧的 x 轴循环进行融合(2+2=4),再将这个融合后的操作进行并行操作。

Var x_outer, y_outer, x_inner, y_inner, tile_index;
gradient.tile(x, y, x_outer, y_outer, x_inner, y_inner, 4, 4);
gradient.fuse(x_outer, y_outer, tile_index);
gradient.parallel(tile_index);

//对应的 C++代码
// This outermost loop should be a parallel for loop, but that's hard in C.
for (int tile_index = 0; tile_index < 4; tile_index++) {
    int y_outer = tile_index / 2;
    int x_outer = tile_index % 2;
    for (int y_inner = 0; y_inner < 4; y_inner++) {
        for (int x_inner = 0; x_inner < 4; x_inner++) {
            int y = y_outer * 4 + y_inner;
            int x = x_outer * 4 + x_inner;
            printf("Evaluating at x = %d, y = %d: %d\n", x, y, x + y);
        }
    }
}

在调度树中使用 Parallel:改变循环类型为并行化;类似还有顺序执行、向量化、循环展开,只需更改相应循环节点上的属性。

在这里插入图片描述

如果用 Halide 实现一个完整的算子,它就是这样的风格:

Func blur_3x3(Func input) {
  Func blur_x, blur_y;
  Var x, y, xi, yi;

  // The algorithm - no storage or order
  blur_x(x, y) = (input(x-1, y) + input(x, y) + input(x+1, y))/3;
  blur_y(x, y) = (blur_x(x, y-1) + blur_x(x, y) + blur_x(x, y+1))/3;

  // The schedule - defines order, locality; implies storage
  blur_y.tile(x, y, xi, yi, 256, 32)
        .vectorize(xi, 8).parallel(y);
  blur_x.compute_at(blur_y, x).vectorize(x, 8);

  return blur_y;
}

其他调度优化

除了这些,其他的优化类型也可以对调度树进行相应的变换。

有效的调度树定义了算法的可能调度空间,而变换是在该空间中的点之间映射的运算符。不同调度树对应了不同的程序实现,具有不同的性能,我们如何能获得一个最优的调度树呢?

最简单的方法就是通过静态分析来获得最优调度树。一旦循环大小确定,我们就有足够的信息来确定调度树的重要执行特征,例如它将分配多少内存和执行多少操作。计调度的成本就是这些数据点的加权总和。

然而这个方法过于简单和天真了,首先我们无法确定每个操作的成本,只可能有一个大概的预在这里插入图片描述
估。其次这些操作是相互影响的,并不独立,也就是成本是动态变化的。成本的总和也并不是简单的线性叠加。总之寻找一个最优调度树是非常复杂的过程,

目前主流的方法如 TVM 中采用的是自动调优法。即根据可利用的优化手段,将它们组合,生成一个十分庞大的调度空间,然后利用一些探索器如启发式算法或者机器学习算法,对这个调度空间进行遍历,去实际运行或者用模型预测其性能,根据实际性能反馈对调度空间的探索,最终在一定时间内选择一个相对最优的调度。

在这里插入图片描述

如果您想了解更多AI知识,与AI专业人士交流,请立即访问昇腾社区官方网站https://www.hiascend.com/或者深入研读《AI系统:原理与架构》一书,这里汇聚了海量的AI学习资源和实践课程,为您的AI技术成长提供强劲动力。不仅如此,您还有机会投身于全国昇腾AI创新大赛和昇腾AI开发者创享日等盛事,发现AI世界的无限奥秘~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2252952.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaSE学习心得(APL与算法篇)

常用APL和常见算法 前言 常用APL Math System Runtime Object ​编辑浅克隆 深克隆 Objects Biginteger 构造方法 成员方法 底层存储方式 Bigdecimal 构造方法 Bigdecimal的使用 底层存储方式 ​编辑正则表达式 两个判断练习 两个爬取练习 贪婪爬取和非贪…

C++ ——— 引用的概念以及特性

目录 引用的概念 引用在实际代码中的作用 引用的特性 1. 引用在定义时必须初始化 2. 一个变量可以有多个引用 3. 可以给别名再次取别名&#xff0c;或者多次取别名 4. 引用一旦引用了实体&#xff0c;就不能再引用其他实体了 引用的概念 引用不是新定义一个变量&#x…

Linux-异步IO和存储映射IO

异步IO 在 I/O 多路复用中&#xff0c;进程通过系统调用 select()或 poll()来主动查询文件描述符上是否可以执行 I/O 操作。而在异步 I/O 中&#xff0c;当文件描述符上可以执行 I/O 操作时&#xff0c;进程可以请求内核为自己发送一个信号。之后进程就可以执行任何其它的任务…

嵌入式入门Day23

数据结构Day4 操作受限的线性表栈基本概念顺序栈顺序栈结构创建顺序栈判空和判满栈扩容入栈出栈遍历销毁栈 链式栈队列基本概念顺序队列循环顺序队列定义循环队列的创建循环顺序队列的判空和判满循环顺序队列的入队循环顺序队列的遍历循环顺序队列的出队循环顺序队列的销毁 链式…

C语言实验 一维数组

时间:2024.12.3 一、实验 7-1 交换最小值和最大值 #include<stdio.h> int main() {int n, a[10], i, min = 0, max = 0;scanf("%d", &n);for (i = 0; i < n; i++){scanf("%d",&a[i]);}for (i = 0; i < n; i++){if (a[min] > a[i…

聚合支付系统官方个人免签系统三方支付系统稳定安全高并发

系统采用fastadmin框架独立全新开发&#xff0c;安全稳定,系统支持代理、商户、码商等业务逻辑。 针对最近一-些JD&#xff0c;TB等业务定制&#xff0c;子账号业务逻辑API 非常详细&#xff0c;方便内置对接! 注意&#xff1a;系统没有配置文档很使用教程&#xff0c;不清楚…

HTMLCSS 奇幻森林:小熊的甜蜜蛋糕派对大冒险

这个 HTML 页面包含了一个背景、多个下落的蛋糕图片和一个左右移动的loopy图片,实现了一个小熊吃蛋糕的效果 演示效果 HTML&CSS <!DOCTYPE html> <html><head><meta charset"utf-8" /><title>ideal life</title><style…

电脑关机的趣味小游戏——system函数、strcmp函数、goto语句的使用

文章目录 前言一. system函数1.1 system函数清理屏幕1.2 system函数暂停运行1.3 system函数电脑关机、重启 二、strcmp函数三、goto语句四、电脑关机小游戏4.1. 程序要求4.2. 游戏代码 总结 前言 今天我们写一点稍微有趣的代码&#xff0c;比如写一个小程序使电脑关机&#xf…

OpenSSL 自建CA 以及颁发证书(网站部署https双向认证)

前言 1、前面写过一篇 阿里云免费ssl证书申请与部署&#xff0c;大家可以去看下 一、openssl 安装说明 1、这部分就不再说了&#xff0c;我使用centos7.9&#xff0c;是自带 openssl的&#xff0c;window的话&#xff0c;要去下载安装 二、CA机构 CA机构&#xff0c;全称为…

在M3上面搭建一套lnmp环境

下载docker-desktop 官网下载docker-desktop 切换镜像源 {"builder": {"gc": {"defaultKeepStorage": "20GB","enabled": true}},"experimental": false,"registry-mirrors": ["https://docke…

WebSocket 通信说明与基于 ESP-IDF 的 WebSocket 使用

一、 WebSocket 出现的背景 最开始 客户端&#xff08;Client&#xff09; 和 服务器&#xff08;Server&#xff09; 通信使用的是 HTTP 协议&#xff0c;HTTP 协议有一个的缺陷为&#xff1a;通信只能由客户端&#xff08;Client&#xff09;发起。 在一些场景下&#xff0…

linux(centos) 环境部署,安装JDK,docker(mysql, redis,nginx,minio,nacos)

目录 1.安装JDK (非docker)1.1 将文件放在目录下&#xff1a; /usr/local/jdk1.2 解压至当前目录1.3 配置环境变量 2.安装docker2.1 验证centos内核2.2 安装软件工具包2.3 设置yum源2.4 查看仓库中所有docker版本&#xff0c;按需选择安装2.5 安装docker2.6 启动docker 并 开机…

CODESYS可视化秒表分批计时详细制作案例(一)

#制作一个在可视化界面可用于秒表计时的详细案例# 前言: 在电脑和手机的时钟上,都有一个秒表计时的功能。除此之外,在赛事上,也有更为专业的秒表计时器设备。举一反三,那么对于工控设备,为了衡量生产效率和节拍,引入了"Cycle Time(CT)"的概念,我们可以通…

openGauss开源数据库实战十八

文章目录 任务十八 openGauss逻辑结构:构:用户和权眼管理任务目标实施步骤一、准备工作二、用户和角色管理1.使用CREATE USER语句创建用户2.使用CREATE ROLE语句创建用户3.删除用户和角色 三、权限管理1.系统权限清理工作 任务十八 openGauss逻辑结构:构:用户和权眼管理 任务目…

Scratch游戏推荐 | 我的世界:平台冒险——像素世界的全新挑战! ⛏️

&#x1f3ae; Scratch游戏推荐 | 我的世界&#xff1a;平台冒险——像素世界的全新挑战&#xff01; ⛏️&#x1f30d; 今天给大家推荐一款精彩绝伦的Scratch平台冒险游戏——《我的世界&#xff1a;平台冒险 – 第二章》&#xff01;由atomicmagicnumber制作&#xff0c;这…

【java-数据结构篇】揭秘 Java LinkedList:链表数据结构的 Java 实现原理与核心概念

我的个人主页 我的专栏&#xff1a;Java-数据结构&#xff0c;希望能帮助到大家&#xff01;&#xff01;&#xff01;点赞❤ 收藏❤ 目录 1. Java LinkedList 基础 1.1 LinkedList 简介 1.2 LinkedList 的实现原理 1.3 LinkedList 与 ArrayList 的区别 2. 链表基础 2.1 链…

北斗道路运输车辆管理应用:违规驾驶行为监测、车辆编队管理、安全跟踪(车辆历史轨迹查询)、车辆动态位置数据的实时查看和管理

文章目录 场景概述解决方案应用案例合作构想场景概述 面向旅游大巴车、危险品运输车及重型载货运输车等车辆,利用北斗定位导航服务,结合互联网通信技术,实现车辆安全驾驶管理与调度,有效降低道路事故发生风险,提升道路运输管理水平及车辆调度能力。 解决方案 在车辆上安…

【ABAP——DAILOG_2】

文章目录 使用Tabstrip控件实现分页签效果标签页的修改使用Table Control控件实现表单输出表格控件使用向导创建Table ControlTable Control列的修改 用户通过界面输入数据&#xff0c;数据通过屏幕控件传递到ABAP/4程序中的变量&#xff0c;程序在PBO中准备数据并显示界面&…

资料文件夹转移工具5.2.3 |快速转移到D盘,释放C盘空间

这是一款支持将C盘的常用文件夹转移到其他磁盘分区的工具&#xff0c;提供仅变目录、复制资料和转移资料三种转移方式。该工具完全免费&#xff0c;单文件免安装&#xff0c;大小仅为546KB&#xff0c;非常适合需要释放C盘空间的用户。 大小&#xff1a;546KB 下载地址&#…

使用STM32CubeMX配置串口各种功能

使用STM32CubeMX配置串口各种功能 STM32CubeMX软件的安装接收空闲中断STM32CubeMX配置1.新建工程2. 选择芯片3. 选择时钟和下载方式4. 配置串口5.设置工程消息6.生成代码7.修改生成的代码 空闲中断DMA转运STM32CubeMX配置4.配置串口5.设置工程消息6.生成代码7.修改生成的代码 S…