├── .github └── workflows │ └── ci.yml ├── .gitignore ├── docs ├── Lab1-MiniCluster │ ├── index.md │ └── pics │ │ ├── 01.png │ │ ├── 02.png │ │ ├── 03.jpg │ │ ├── 04.jpg │ │ ├── 05.jpg │ │ ├── 06.jpg │ │ ├── 07.jpg │ │ └── image-20210714120624669.png ├── Lab2-Vectors │ ├── .gitignore │ ├── index.assets │ │ ├── bilinear.png │ │ └── bilinear2.png │ ├── index.md │ └── starter_code │ │ ├── bilinear_interp │ │ ├── baseline.py │ │ └── vectorized.py │ │ ├── main.py │ │ └── utils │ │ └── timer.py ├── Lab2.5-Vectors-Bonus │ ├── add.cpp │ └── index.md ├── Lab3-Cuda │ ├── img │ │ ├── API.png │ │ ├── block_part.png │ │ ├── conv.png │ │ ├── env_info.png │ │ └── shared_memory.png │ ├── index.md │ └── starter_code │ │ ├── Makefile │ │ └── conv.cu ├── Lab4-Gemm │ ├── code │ │ ├── Makefile │ │ ├── README.md │ │ ├── hw.h │ │ └── hw_baseline.cpp │ ├── index.assets │ │ ├── 225px-SIMD.svg.png │ │ ├── array-packing.png │ │ ├── image-20220703230249870.png │ │ ├── username.txt │ │ └── v2-37cd14433f0a64844ccd435f3b48b236_r.jpg │ └── index.md ├── Lab5-DL │ ├── index.assets │ │ ├── CIFAR.png │ │ ├── Data-Parallelism.png │ │ ├── LeNet.jpg │ │ ├── MNIST.jpeg │ │ ├── Pipeline-Parallelism.png │ │ ├── Tensor-Parallelism.png │ │ └── transformer.png │ └── index.md └── index.md └── mkdocs.yml /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: gh-pages 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-python@v2 12 | with: 13 | python-version: 3.x 14 | - run: pip install mkdocs-material 15 | - run: mkdocs gh-deploy --force 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | site/* 2 | .vscode/ 3 | -------------------------------------------------------------------------------- /docs/Lab1-MiniCluster/index.md: -------------------------------------------------------------------------------- 1 | # 实验一:简单集群搭建 2 | 3 | ## 1 实验简介 4 | 5 | 本次实验要求使用四台虚拟机搭建一个简易的集群,并对该集群进行性能测试,最后提交测试结果和实验报告。 6 | 7 | 集群搭建的任务包括创建虚拟机、安装 Linux 发行版、配置网络和 ssh 通信。 8 | 9 | 性能测试通过使用 OpenMPI 将 HPL 测试程序分配到四个虚拟机节点上执行。因此,需要下载并编译 OpenMPI、BLAS 和 HPL 的源代码,其中 OpenMPI、BLAS是 HPL 的依赖项。 10 | 11 | ## 2 实验环境 12 | 13 | - 一台计算机,操作系统任意 14 | - Hypervisor (本手册为 Virtual Box) 15 | - 虚拟机 * 4 16 | 17 | ## 3 实验基础知识介绍 18 | 19 | ### 3.1 计算机集群 20 | 21 | [计算机集群](https://en.wikipedia.org/wiki/Computer_cluster)是连接在一起、协同工作的一组计算机,集群中的每个计算机都是一个节点。在集群中,由软件将不同的计算任务(task)分配(schedule)到相应的一个或一群节点(node)上。本次实验中,需要使用 OpenMPI 将 HPL 程序作为 task 分配到集群中的四个节点上。 22 | 23 | #### 3.1.1 虚拟机 24 | 25 | 虚拟机为运行在其中的guest操作系统和应用提供了一个模拟的硬件环境,和真实的硬件保持一样的接口和表现,同时也如真实的硬件一样为其中的操作系统和程序提供保护机制、管理接口和资源限制。一个简易的非虚拟机和虚拟机结构的对比如下图(来源:Abraham Silberschatz, Peter Baer Galvin, Greg Gagn, *Operating System Concepts*, 10th edition, Chapter 18) 26 | 27 | 一种常见的虚拟机机制实现方式便是通过 hypervisor(又称VMM: Virtual Machnie Manager)来为 guest 操作系统提供模拟硬件环境,这也为在一台物理机上运行多个虚拟机提供了可能。 28 | 29 | 本手册中使用 Virtural Box 作为 hypervisor 进行示范和说明。 30 | 31 | ![](pics/image-20210714120624669.png) 32 | 33 | #### 3.1.2 Linux发行版 34 | 35 | Linux 发行版(也被叫做 GNU/Linux 发行版),为一般用户预先集成好的 Linux 操作系统及各种应用 软件。一般用户不需要重新编译,在直接安装之后,只需要小幅度更改设置就可以使用,通常以软件包管理系统来进行应用软件的管理。Linux 发行版通常包含了包括桌面环境、办公包、媒体播放器、数据库等应用软件。这些操作系统通常由 Linux 内核、以及来自 GNU 计划的大量的函数库,和基于 X Window 的图形界面。现在有超过 300 个 Linux发行版。大部分都正处于活跃的开发中,不断地改进。由于大多数软件包是自由软件和开源软件,所以 Linux 发行版的形式多种多样——从功能齐全的桌面系统以及服务器系统到小型系统 (例如一些嵌入式设备)。除了一些定制软件 (如安装和配置工具),发行版通常只是将特定的应用软件安装在一堆函数库和内核上,以满足特定用户的需求。 36 | 37 | 这些发行版可以分为商业发行版,比如 Ubuntu(Canonical 公司)、Fedora(Red Hat)、openSUSE (Novell)和 Mandriva Linux;和社区发行版,它们由自由软件社区提供支持,如 Debian 和 Gentoo;也有发行版既不是商业发行版也不是社区发行版,如 Slackware。 38 | 39 | ### 3.2 HPL 40 | 41 | HPL是一个可以在分布式系统上运行的解稠密线性系统的软件包,同时也可以被用来做高性能计算Linpack测试(High Performance Computing Linpack Benchmark)。 42 | 43 | 关于HPL的详细介绍可参考 [https://www.netlib.org/benchmark/hpl/](https://www.netlib.org/benchmark/hpl/) 44 | 45 | > The HPL software package **requires** the availibility on your system of an implementation of the Message Passing Interface **MPI** (1.1 compliant). An implementation of **either** the Basic Linear Algebra Subprograms **BLAS or** the Vector Signal Image Processing Library **VSIPL** is also needed. Machine-specific as well as generic implementations of [MPI](https://www.netlib.org/benchmark/hpl/links.html#mpi_libs), the [BLAS](https://www.netlib.org/benchmark/hpl/links.html#blas_libs) and [VSIPL](https://www.netlib.org/benchmark/hpl/links.html#vsip_libs) are available for a large variety of systems. 46 | 47 | HPL 需要系统中有 MPI 实现和 BLAS 实现,因此我们需要在安装 HPL 前在虚拟机中安装 OpenMPI 和 BLAS。 48 | 49 | OpenMPI 是一个开源的 [Message Passing Interface](http://www.mpi-forum.org/) 实现,由一些科研机构和企业一起开发和维护。MPI 是一套标准化、可移植的消息传递标准,它被设计用于支持并行计算系统的架构,使得开发者能够方便地开发可移植的消息传递程序。同时,MPI 编程能力在高性能计算的实践与学习中也是非常基础的技能。 50 | 51 | BLAS 是 Basic Linear Algebra Subprograms 的缩写,本手册只要求将其作为 HPL 的依赖项下载安装即可,无需过多了解。 52 | 53 | ## 4 实验步骤 54 | 55 | ### 4.1 下载 Hypervisor 和 Linux 光盘映像文件 56 | 57 | #### 4.1.1 Hypervisor 58 | 59 | 在创建虚拟机之前,你需要先准备好 Hypervisor,在本手册中,我们以 Virtual Box 为例,其他的 Hypervisor 请自行参阅相关材料: 60 | 61 | - [Virtual Box 官网下载](https://www.virtualbox.org/wiki/Downloads): 62 | 63 | ![Virtual Box 官网](pics/01.png) 64 | 65 | > 注意**不要选错宿主机平台**! 66 | 67 | - Docker 68 | 69 | 由于本次实验希望大家从裸机手工完成完整的集群配置,包括网络和系统软件环境等,因此本次实验不推荐大家使用 Docker,如果学有余力可以尝试使用 Docker 复现本次实验,作为加分项(虚拟机为必做)。 70 | 71 | #### 4.1.2 Linux 光盘映像文件 72 | 73 | 本手册所使用的范例发行版是 Debian 11,同学可根据自己的喜好和经验挑选适合的发行版。 74 | Debian 下载点(如果网速问题可访问国内镜像): 75 | 76 | 77 | - [ZJU Mirror](https://mirrors.zju.edu.cn/debian-cd/current-live/amd64/iso-hybrid/) 78 | - [Tuna Mirror](https://mirrors.tuna.tsinghua.edu.cn/debian-cd/current-live/amd64/iso-hybrid/) 79 | - [Official Mirror](https://cdimage.debian.org/debian-cd/current-live/amd64/iso-hybrid/) 80 | 81 | ![Debian 下载](pics/02.png) 82 | 83 | > 花花绿绿的映像,该怎么选择好呢?图中带有 mate 字眼的映像代表附有 mate 桌面,为了最精简安装,本手册采 standard 版本(不含桌面)。 84 | 85 | - 不推荐使用的光盘映像 86 | 87 | - 从互联网下载 (关键字:Install via Internet / netinstall) 88 | 89 | 此类镜像是在安装过程中访问互联网下载需要的包,由于虚拟机可能无法访问互联网,部分镜像虽然体积小但缺少很多重要的命令,因此不要挑选此类镜像。 90 | 91 | - 带有桌面环境的光盘映像 (关键字:Desktop / 以及后面带有 gnome, kde, lxde 等词的映像) 92 | 93 | 如果计算机上的磁盘空间较不充足,建议不要下载带有桌面环境的光盘映像,如果你偏好桌面且空间足够可以忽略。 94 | 95 | - 你不熟悉的光盘映像 / 没有图形界面(GUI)安装程序的光盘映像 96 | 97 | Linux 发行版相当多,不熟悉或没使用过 Linux 的同学建议参考本手册,相信自己已经有一定基础的同学可以忽略。 98 | 99 | ### 4.2 搭建集群并安装相关程序 100 | 101 | #### 4.2.1 创建虚拟机 102 | 103 | 准备好 Hypervisor 跟 光盘映像后,就可以着手安装一个虚拟机了,请参考 Virtual Box 手册和相关教程。 104 | 105 | - 选择发行版、内存、磁盘空间 106 | 107 | Step1-1 108 | Step1-2 109 | 110 | 选择自己要安装的发行版(如果没有直接选 Linux 即可),内存和磁盘空间根据实际情况分配。 111 | 112 | - 插入发行版映像文件和配置网络 113 | 114 | Step2-1 115 | 116 | 选取刚下载的映像文件。 117 | 118 | Step2-2 119 | 120 | 网络对虚拟机来说十分复杂,如果你不熟悉相关的名词,在一台虚拟机的情况下,直接默认的 NAT 即可。选择 NAT 的另一个好处是,因为手册需要节点间彼此互连,因此使用 NAT Network 的同时,所有节点也可访问互联网。 121 | 122 | 不需要互联网或者使用有线网的同学也可以考虑使用 Bridged 或 Internal Network,具体请参考手册上的说明:[Virtual Networking](https://www.virtualbox.org/manual/ch06.html)。 123 | 124 | > 小贴士 125 | > - 如果你的虚拟机无法连上网,可能是宿主机的问题,请**排查宿主机的网络状况**。 126 | > - 由于学校的网络情况,在学校中使用虚拟机的同学还是**以 NAT Network 为主**较方便。 127 | 128 | - 进入图形安装程序 129 | 130 | 在启动虚拟机后,不要直接进入 Live CD,直接进入安装程序,照着引导走即可。 131 | 132 | ![Step3-1](pics/07.jpg) 133 | 134 | - 安装完成并重启 135 | 136 | 在刚刚插入映像文件的地方取消选取映像文件,否则下次重启时还会进入 Live CD。 137 | 138 | #### 4.2.2 下载并安装 OpenMPI 139 | 140 | 由于系统中的包通常比较旧,因此我们从 OpenMPI 的官网中下载最新版本源码自行编译安装:[OpenMPI 下载](https://www.open-mpi.org/software/ompi/v4.1/)。 141 | 142 | > 小贴士 143 | > - 在虚拟机**可联网**的环境下,可直接使用 `wget` 或 `curl` 来下载,会方便许多。 144 | > - 在编译前,先了解 `make` 和 `autoconf`,可以减少不少除错时间。 145 | 146 | - 修改 `PATH` 和 `LD_LIBRARY_PATH` 147 | 148 | 请将找到 OpenMPI 二进制文件的目录加入 `PATH` 环境变量,OpenMPI 库的目录加入 `LD_LIBRARY_PATH` 环境变量。 149 | 150 | #### 4.2.3 下载并安装 HPL 151 | 152 | 下载地址:[https://netlib.org/benchmark/hpl/software.html](https://netlib.org/benchmark/hpl/software.html) 153 | 154 | - BLAS 155 | 156 | HPL 的依赖除了一个 MPI 实现(在手册中是 OpenMPI)外,还需要一个 BLAS 实现,我们可以从 netlib 下载[其中一个实现](https://www.netlib.org/blas/#_software),虽然没有优化过,但拿来测试已经足够了。 157 | 158 | - 检查 `gcc` / `gfortran` 环境 159 | 160 | BLAS 需要 `gcc` / `gfortran` 来编译,请务必检查自己虚拟机中编译器是否存在及其版本。 161 | 162 | - 编译 BLAS/CBLAS 163 | 164 | 先编译 BLAS,再参考 `README` 和 `INSTALL` 修改 CBLAS 的 Makefile 并编译 (需要 BLAS 的链接文件)。 165 | 166 | - 修改 HPL Makefile 167 | 168 | 解压 HPL 压缩包后,在根目录的 `setup/` 文件夹下有 Makefile 相关文件的模板(Make.xxx,后缀代表架构),复制到根目录并保存。 169 | 170 | 参考 `README` ,根据自己情况修改以下参数: 171 | 172 | ``` Makefile 173 | # Make.test 174 | # arch 175 | ARCH = test 176 | ... 177 | # MPI 178 | MPdir = /path/to/your/mpi/ 179 | MPinc = -I$(MPdir)/include64 180 | MPlib = $(MPdir)/libmpi.so 181 | # BLAS 182 | LAdir = /path/to/your/blas 183 | LAinc = 184 | LAlib = $(LAdir)/lib 185 | ... 186 | # compiler 187 | CC = /path/to/your/mpicc 188 | LINKER = $(CC) 189 | ``` 190 | 191 |   修改当前目录下 `Make.top` 中的 `arch` 参数,需与 `Make.test` 中的一致。 192 | 193 | - 编译 194 | 195 | ```shell 196 | # 替换成你自己的后缀 197 | make arch=test 198 | ``` 199 | 200 | #### 4.2.4 克隆节点 201 | 202 | 在 Virtual Box 中,克隆已经配置完成的节点成为集群中的其他节点,本手册范例中仅克隆一个(集群中两个节点),可克隆更多。 203 | 204 | ### 4.3 测试集群 205 | 206 | #### 4.3.1 ping 207 | 208 | `ping` 是测试节点网络连通性最为简单的方式,在进行其他测试前,请先确认能 `ping` 通所有节点。 209 | 210 | #### 4.3.2 配置 ssh 211 | 212 | `ssh` 是相当常用的,实现安全远程连接的方式,其原理和使用、配置方法请查阅相关参考资料:[Open SSH 网站](https://www.openssh.com/manual.html) 213 | 如果你没有 ssh 密钥,可以在其中一个节点创建一个: 214 | 215 | ```shell 216 | ssh-keygen 217 | ``` 218 | 我们需要将自己的 ssh 公钥复制一份到另一个节点上的 `.ssh/authorized_keys` 中(可以利用 `ssh-copy-id` 命令来拷贝公钥,也可以直接使用 `nc` 将公钥作为文件传输)。复制完成后,注意检查 `authorized_keys` (600) 和 `.ssh/` (700) 目录的权限,否则无法顺利 `ssh`。 219 | 220 | - ssh passphrase 221 | 222 | 如果自己的密钥有 passphrase,那么请使用 `ssh-agent` 确保能暂时不用输入 passphrase,以免之后影响 `mpirun` 正确运行。 223 | 224 | #### 4.3.3 mpirun 225 | 226 | 尽管对 OpenMPI 十分陌生,我们还是能利用它来跑非 MPI 的程序,同学可以编写简单的 Helloworld 程序来测试 OpenMPI,或者直接使用 Unix 命令。 227 | 228 | - OpenMPI hostfile 229 | 230 | 两个节点都准备好后,我们可以试试 `mpirun` 了!按照如下格式,编写 MPI 的 hostfile 并保存: 231 | ``` 232 | localhost slots=1 233 | 10.0.2.5 slots=1 234 | ``` 235 | 其中 slots 代表的意思是一个节点的 CPU 有多少核,由于我们创建虚拟机时仅分配一个核,因此这里的 slots 上限为 1 ,同学根据自己虚拟机的情况,修改 slots 的值。 236 | 237 | - 测试程序 238 | 239 | OpenMPI 需要测试程序为节点所共有或在节点上有相同路径,因为我们的第二个节点是克隆出来的,因此两个节点上的命令和 HPL 程序都会是相同路径, 240 | 此时运行以下命令就能看到每个节点上线了多久: 241 | 242 | ``` 243 | mpirun --hostfile myhostfile uptime 244 | ``` 245 | 246 | - 运行 HPL: 247 | 248 | ``` 249 | mpirun --hostfile myhostfile ./xhpl 250 | ``` 251 | 252 | ## 5 实验任务与要求 253 | 254 | 1. 搭建四个节点的虚拟机并记录过程,要求提供必要的截图或配置文件 255 | 2. 使用 OpenMPI 和 HPL 测试集群表现并记录结果 256 | -------------------------------------------------------------------------------- /docs/Lab1-MiniCluster/pics/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZJUSCT/HPC101-Labs-2022/c3651b7fc444a9f064bc5273231502a73cf7aaa3/docs/Lab1-MiniCluster/pics/01.png -------------------------------------------------------------------------------- /docs/Lab1-MiniCluster/pics/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZJUSCT/HPC101-Labs-2022/c3651b7fc444a9f064bc5273231502a73cf7aaa3/docs/Lab1-MiniCluster/pics/02.png -------------------------------------------------------------------------------- /docs/Lab1-MiniCluster/pics/03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZJUSCT/HPC101-Labs-2022/c3651b7fc444a9f064bc5273231502a73cf7aaa3/docs/Lab1-MiniCluster/pics/03.jpg -------------------------------------------------------------------------------- /docs/Lab1-MiniCluster/pics/04.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZJUSCT/HPC101-Labs-2022/c3651b7fc444a9f064bc5273231502a73cf7aaa3/docs/Lab1-MiniCluster/pics/04.jpg -------------------------------------------------------------------------------- /docs/Lab1-MiniCluster/pics/05.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZJUSCT/HPC101-Labs-2022/c3651b7fc444a9f064bc5273231502a73cf7aaa3/docs/Lab1-MiniCluster/pics/05.jpg -------------------------------------------------------------------------------- /docs/Lab1-MiniCluster/pics/06.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZJUSCT/HPC101-Labs-2022/c3651b7fc444a9f064bc5273231502a73cf7aaa3/docs/Lab1-MiniCluster/pics/06.jpg -------------------------------------------------------------------------------- /docs/Lab1-MiniCluster/pics/07.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZJUSCT/HPC101-Labs-2022/c3651b7fc444a9f064bc5273231502a73cf7aaa3/docs/Lab1-MiniCluster/pics/07.jpg -------------------------------------------------------------------------------- /docs/Lab1-MiniCluster/pics/image-20210714120624669.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZJUSCT/HPC101-Labs-2022/c3651b7fc444a9f064bc5273231502a73cf7aaa3/docs/Lab1-MiniCluster/pics/image-20210714120624669.png -------------------------------------------------------------------------------- /docs/Lab2-Vectors/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | __pycache__ 3 | .DS_Store -------------------------------------------------------------------------------- /docs/Lab2-Vectors/index.assets/bilinear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZJUSCT/HPC101-Labs-2022/c3651b7fc444a9f064bc5273231502a73cf7aaa3/docs/Lab2-Vectors/index.assets/bilinear.png -------------------------------------------------------------------------------- /docs/Lab2-Vectors/index.assets/bilinear2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZJUSCT/HPC101-Labs-2022/c3651b7fc444a9f064bc5273231502a73cf7aaa3/docs/Lab2-Vectors/index.assets/bilinear2.png -------------------------------------------------------------------------------- /docs/Lab2-Vectors/index.md: -------------------------------------------------------------------------------- 1 | # 实验二:向量化计算 2 | 3 | ## 1 实验简介 4 | 5 | [NumPy](https://numpy.org/) 是 Python 中科学计算的基础包。它是一个 Python 库,提供多维数组对象,各种派生对象(如掩码数组和矩阵),以及用于数组快速操作的各种 API,有包括数学、逻辑、形状操作、排序、选择、输入输出、离散傅立叶变换、基本线性代数,基本统计运算和随机模拟等等。 6 | 7 | Numpy 代码一般采用向量化(矢量化)描述,这使得代码中没有任何显式的循环,索引等,这样的代码有以下好处: 8 | 9 | - 向量化代码更简洁,更易于阅读 10 | - 更少的代码行通常意味着更少的错误 11 | - 代码更接近于标准的数学符号 12 | 13 | 另外,向量化的代码能够规避掉 Python 中缓慢的迭代循环,被底层的实现更好的调度,如接入 BLAS 矩阵运算库,从而实现更高的性能。 14 | 15 | 双线性插值是计算机视觉图像处理中的常用算法,它在计算机图形学中也可以用于材质贴图的重采样。 16 | 17 | 本次实验我们将借助 NumPy 实现一个支持批量处理的向量化的双线性插值,来让大家熟悉 NumPy 的向量化编程模式。 18 | 19 | 20 | 21 | ## 2 实验环境 22 | 23 | - 任何含有 Python3 和 NumPy 的环境 24 | 25 | 26 | 27 | ## 3 实验基础知识介绍 28 | 29 | ### 3.1 NumPy API 30 | 31 | 由于课上已经介绍过这部分内容,因此我们在实验手册中略去。具体可以参考 NumPy 的[文档](https://numpy.org/doc/stable/)。 32 | 33 | ### 3.2 双线性插值算法 34 | 35 | 双线性插值的算法其实非常简单,概括来说就是先在 $x$ 轴上进行一次插值,再在 $y$ 轴上进行一次插值。 36 | 37 | ![bilinear2](index.assets/bilinear2.png) 38 | 39 | 以在灰度图上进行插值为例,我们已知外围的四个点 $(14, 20), (15, 20), (14, 21), (15, 21)$ 灰度值分别为 91, 210, 162 和 95,然后希望通过插值得到 $(14.5, 20.2)$ 处的灰度值。 40 | 41 | 接下来我们先在 $x$ 方向上通过线性插值计算出 $(14.5, 20), (14.5, 21)$ 两个点的灰度值 150.5, 128.5,然后再使用这两个值在 $y$ 方向上再次进行线性插值,得到 $(14.5, 20.2)$ 坐标处的灰度值 146.1。 42 | 43 | 注意这里是一个单通道的例子,对于实际的情况,我们往往有很多个通道,如彩色图片拥有 RGB 三个通道,一些图片可能还有 $\alpha$ 透明度通道,或是深度通道。**对于多通道的情况,我们需要对每个通道进行分别插值。** 44 | 45 | #### 3.2.1 形式化定义 46 | 47 | > 形式化定义摘自[维基百科](https://en.wikipedia.org/wiki/Bilinear_interpolation) 48 | 49 | 假如我们想得到未知函数 $f$ 在点 ${\displaystyle P=\left(x,y\right)}$ 的值,假设我们已知函数 $f$ 在 ${\displaystyle Q_{11}=\left(x_{1},y_{1}\right)}$, ${\displaystyle Q_{12}=\left(x_{1},y_{2}\right)}$, ${\displaystyle Q_{21}=\left(x_{2},y_{1}\right)}$ 及 ${Q_{22}=\left(x_{2},y_{2}\right)}$ 四个点的值。 50 | 51 | ![bilinear](index.assets/bilinear.png) 52 | 53 | 首先在 $x$ 方向进行线性插值,得到 54 | $$ 55 | {\displaystyle {\begin{aligned}f(x,y_{1})&\approx {\frac {x_{2}-x}{x_{2}-x_{1}}}f(Q_{11})+{\frac {x-x_{1}}{x_{2}-x_{1}}}f(Q_{21}),\\\\ 56 | f(x,y_{2})&\approx {\frac {x_{2}-x}{x_{2}-x_{1}}}f(Q_{12})+{\frac {x-x_{1}}{x_{2}-x_{1}}}f(Q_{22}).\end{aligned}}} 57 | $$ 58 | 然后在 $y$ 方向进行线性插值,得到 59 | $$ 60 | {\displaystyle {\begin{aligned}f(x,y)&\approx &&{\frac {y_{2}-y}{y_{2}-y_{1}}}f(x,y_{1})+{\frac {y-y_{1}}{y_{2}-y_{1}}}f(x,y_{2})\\\\ 61 | &=&&{\frac {y_{2}-y}{y_{2}-y_{1}}}\left({\frac {x_{2}-x}{x_{2}-x_{1}}}f(Q_{11})+{\frac {x-x_{1}}{x_{2}-x_{1}}}f(Q_{21})\right)\\\\ 62 | &&&+{\frac {y-y_{1}}{y_{2}-y_{1}}}\left({\frac {x_{2}-x}{x_{2}-x_{1}}}f(Q_{12})+{\frac {x-x_{1}}{x_{2}-x_{1}}}f(Q_{22})\right)\\\\ 63 | &=&&{\frac {1}{(x_{2}-x_{1})(y_{2}-y_{1})}}{\big (}f(Q_{11})(x_{2}-x)(y_{2}-y)+f(Q_{21})(x-x_{1})(y_{2}-y)\\\\ 64 | &&&+f(Q_{12})(x_{2}-x)(y-y_{1})+f(Q_{22})(x-x_{1})(y-y_{1}){\big )}\\\\ 65 | &=&&{\frac {1}{(x_{2}-x_{1})(y_{2}-y_{1})}}{\begin{bmatrix}x_{2}-x&x-x_{1}\end{bmatrix}}{\begin{bmatrix}f(Q_{11})&f(Q_{12})\\\\ 66 | f(Q_{21})&f(Q_{22})\end{bmatrix}}{\begin{bmatrix}y_{2}-y\\\\ y-y_{1}\end{bmatrix}}.\end{aligned}}} 67 | $$ 68 | 注意此处如果先在 $y$ 方向插值、再在 $x$ 方向插值,其结果与按照上述顺序双线性插值的结果是一样的。 69 | 70 | ### 3.3 NHWC 数据格式 71 | 72 | 真实情况下我们处理的数据都是以 batch 为单位的,按批进行处理的。以双线性插值为例,我们往往会一次性送入 $N$ 张大小为 $H \times W$ 的图片,每个像素上有 $C$ 个通道,然后一次性返回这 $N$ 张图片处理好的结果。此时我们一次性传入的数据,就是直接按顺序堆叠在一起的 NHWC 格式的数组,它将 batch 作为了第一个维度,而后三个维度分别是单张图片的高度、宽度、通道数。你可以将这一数据格式理解为 c 语言中的高维数组 `image[N][H][W][C]`,而因为 c 的数组和 NumPy 的 `ndarray` 一样都是在内存里连续排放的,所以对于 `image[x1][x2][x3][x4]`,其实就是 `image[x1 * H * W * C + x2 * W * C + x3 * C + x4]` 处的内存。 73 | 74 | 另一个常见的数据格式是 NCHW,也就是单张图片内通道在前,不过这里我们没有选用。 75 | 76 | 数据格式更多是对数据的存放顺序进行约定,你可以通过 `np.transpose` 将不同维度进行调换。 77 | 78 | 79 | 80 | ## 4 实验步骤 81 | 82 | ### 4.1 接口定义 83 | 84 | ```python linenums="1" 85 | def bilinear_interp(a: np.ndarray, b: np.ndarray) -> np.ndarray: 86 | """ 87 | - a is a ND array with shape [N, H1, W1, C] 88 | - b is a ND array with shape [N, H2, W2, 2] 89 | - return a ND array with shape [N, H2, W2, C] 90 | """ 91 | ``` 92 | 93 | 其含义是,对于 batch 内的每一张 $H1\times W1$ 的图 a',b' 中给出了新的 $H2\times W2$ 的图中每个像素所想要采样的 a' 图中对应点的坐标,并将采样结果返回。 94 | 95 | **为了简化任务,我们假定传入的采样点不会出现在 $(H1 - 1, W1 - 1)$,即图像的右下角。** 96 | 97 | ### 4.2 基准代码 98 | 99 | 下面给出直接使用 `for` 循环迭代计算的双线性插值版本: 100 | 101 | ```python linenums="1" 102 | def bilinear_interp_baseline(a: np.ndarray, b: np.ndarray) -> np.ndarray: 103 | """ 104 | This is the baseline implementation of bilinear interpolation without vectorization. 105 | - a is a ND array with shape [N, H1, W1, C], dtype = int64 106 | - b is a ND array with shape [N, H2, W2, 2], dtype = float64 107 | - return a ND array with shape [N, H2, W2, C], dtype = int64 108 | """ 109 | # Get axis size from ndarray shape 110 | N, H1, W1, C = a.shape 111 | N1, H2, W2, _ = b.shape 112 | assert N == N1 113 | 114 | # Do iteration 115 | res = np.empty((N, H2, W2, C), dtype=int64) 116 | for n in range(N): 117 | for i in range(H2): 118 | for j in range(W2): 119 | x, y = b[n, i, j] 120 | x_idx, y_idx = int(np.floor(x)), int(np.floor(y)) 121 | _x, _y = x - x_idx, y - y_idx 122 | # For simplicity, we assume: 123 | # - all x are in [0, H1 - 1) 124 | # - all y are in [0, W1 - 1) 125 | res[n, i, j] = a[n, x_idx, y_idx] * (1 - _x) * (1 - _y) + \ 126 | a[n, x_idx + 1, y_idx] * _x * (1 - _y) + \ 127 | a[n, x_idx, y_idx + 1] * (1 - _x) * _y + \ 128 | a[n, x_idx + 1, y_idx + 1] * _x * _y 129 | return res 130 | ``` 131 | 132 | ### 4.3 完成向量化实现 133 | 134 | 在我们给出的代码的 `bilinear_interp/vectorized.py` 中,完成 `bilinear_interp_vectorized` 函数。 135 | 136 | ### 4.4 检测实现正确与加速效果 137 | 138 | 运行 `main.py`,查看输出,一切顺利将看到以下结果: 139 | 140 | ``` 141 | Generating Data... 142 | Executing Baseline Implementation... 143 | Finished in 139.50709176063538s 144 | Executing Vectorized Implementation... 145 | Finished in 4.717759132385254s 146 | [PASSED] Results are identical. 147 | Speed Up 29.570626190511327x 148 | ``` 149 | 150 | 否则将会触发异常: 151 | 152 | ``` 153 | Traceback (most recent call last): 154 | File "Introduction-Labs-2021/lab2_vectors/main.py", line 28, in 155 | raise Exception('Results are different!') 156 | Exception: Results are different! 157 | ``` 158 | 159 | 其中,耗时与加速比与你的实现和设备有关(主要是实现)。请在报告中将你的运行结果贴出。 160 | 161 | ## 5 实验初始代码 162 | 163 | 详见 [starter_code](https://github.com/ZJUSCT/HPC101-Labs-2022/blob/main/docs/Lab2-Vectors/starter_code) 164 | 165 | ## 6 实验任务与要求 166 | 167 | 1. 完成双线性插值的向量化版本 168 | 2. 测试向量化实现的正确性和加速比 169 | 3. 提交以下内容: 170 | 1. 代码 171 | 2. 简单实验报告,包含 172 | 1. 思路 173 | 2. 正确性与加速比 174 | 175 | 176 | ## 参考资料 177 | 178 | - 关于双线性插值:[https://en.wikipedia.org/wiki/Bilinear_interpolation](https://en.wikipedia.org/wiki/Bilinear_interpolation) 179 | - NumPy 文档:[https://numpy.org/doc/stable/](https://numpy.org/doc/stable/) 180 | - 一篇不错的入门教程:[https://medium.com/better-programming/numpy-illustrated-the-visual-guide-to-numpy-3b1d4976de1d](https://medium.com/better-programming/numpy-illustrated-the-visual-guide-to-numpy-3b1d4976de1d) 181 | - 一篇稍微硬核一点的教程:[https://www.labri.fr/perso/nrougier/from-python-to-numpy/](https://www.labri.fr/perso/nrougier/from-python-to-numpy/) 182 | - 更多练习:[https://github.com/rougier/numpy-100](https://github.com/rougier/numpy-100) 183 | -------------------------------------------------------------------------------- /docs/Lab2-Vectors/starter_code/bilinear_interp/baseline.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numpy import int64 3 | 4 | 5 | def bilinear_interp_baseline(a: np.ndarray, b: np.ndarray) -> np.ndarray: 6 | """ 7 | This is the baseline implementation of bilinear interpolation without vectorization. 8 | - a is a ND array with shape [N, H1, W1, C], dtype = int64 9 | - b is a ND array with shape [N, H2, W2, 2], dtype = float64 10 | - return a ND array with shape [N, H2, W2, C], dtype = int64 11 | """ 12 | # Get axis size from ndarray shape 13 | N, H1, W1, C = a.shape 14 | N1, H2, W2, _ = b.shape 15 | assert N == N1 16 | 17 | # Do iteration 18 | res = np.empty((N, H2, W2, C), dtype=int64) 19 | for n in range(N): 20 | for i in range(H2): 21 | for j in range(W2): 22 | x, y = b[n, i, j] 23 | x_idx, y_idx = int(np.floor(x)), int(np.floor(y)) 24 | _x, _y = x - x_idx, y - y_idx 25 | # For simplicity, we assume all x are in [0, H1 - 1), all y are in [0, W1 - 1) 26 | res[n, i, j] = a[n, x_idx, y_idx] * (1 - _x) * (1 - _y) + a[n, x_idx + 1, y_idx] * _x * (1 - _y) + \ 27 | a[n, x_idx, y_idx + 1] * (1 - _x) * _y + a[n, x_idx + 1, y_idx + 1] * _x * _y 28 | return res 29 | -------------------------------------------------------------------------------- /docs/Lab2-Vectors/starter_code/bilinear_interp/vectorized.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numpy import int64 3 | 4 | 5 | def bilinear_interp_vectorized(a: np.ndarray, b: np.ndarray) -> np.ndarray: 6 | """ 7 | This is the vectorized implementation of bilinear interpolation. 8 | - a is a ND array with shape [N, H1, W1, C], dtype = int64 9 | - b is a ND array with shape [N, H2, W2, 2], dtype = float64 10 | - return a ND array with shape [N, H2, W2, C], dtype = int64 11 | """ 12 | # get axis size from ndarray shape 13 | N, H1, W1, C = a.shape 14 | N1, H2, W2, _ = b.shape 15 | assert N == N1 16 | 17 | # TODO: Implement vectorized bilinear interpolation 18 | return np.empty((N, H2, W2, C), dtype=int64) 19 | -------------------------------------------------------------------------------- /docs/Lab2-Vectors/starter_code/main.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from bilinear_interp.baseline import bilinear_interp_baseline 4 | from bilinear_interp.vectorized import bilinear_interp_vectorized 5 | 6 | from utils.timer import time_function 7 | 8 | N = 8 9 | H1 = 540 10 | W1 = 960 11 | C = 4 12 | scale = 256 13 | H2 = 720 14 | W2 = 1280 15 | 16 | 17 | if __name__ == '__main__': 18 | # Generate random data 19 | print('Generating Data...') 20 | a = np.random.randint(scale, size=(N, H1, W1, C)) 21 | b = np.random.rand(N, H2, W2, 2) * [H1 - 1, W1 - 1] 22 | assert np.max(b[:, :, :, 0]) < H1 23 | assert np.max(b[:, :, :, 1]) < W1 24 | 25 | # Call Bilinear Interpolation Implementations 26 | print('Executing Baseline Implementation...') 27 | baseline_result, baseline_time = time_function(bilinear_interp_baseline, a, b) 28 | print(f'Finished in {baseline_time}s') 29 | 30 | print('Executing Vectorized Implementation...') 31 | vectorized_result, vectorized_time = time_function(bilinear_interp_vectorized, a, b) 32 | print(f'Finished in {vectorized_time}s') 33 | 34 | # Check Results 35 | if not np.array_equal(baseline_result, vectorized_result): 36 | raise Exception('Results are different!') 37 | else: 38 | print("[PASSED] Results are identical.") 39 | print(f"Speed Up {baseline_time / vectorized_time}x") 40 | -------------------------------------------------------------------------------- /docs/Lab2-Vectors/starter_code/utils/timer.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | def time_function(f, *args): 5 | """ 6 | Call a function f with args and return the time (in seconds) that it took to execute. 7 | """ 8 | tic = time.time() 9 | res = f(*args) 10 | toc = time.time() 11 | return res, toc - tic 12 | -------------------------------------------------------------------------------- /docs/Lab2.5-Vectors-Bonus/add.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #define MAXN 100000000 7 | 8 | float a[MAXN]; 9 | float b[MAXN]; 10 | float c[MAXN]; 11 | float d[MAXN]; 12 | 13 | int main() 14 | { 15 | for (int i = 0; i < MAXN; ++i) 16 | { 17 | a[i] = 1.0 / (rand() + 1); 18 | b[i] = 1.0 / (rand() + 1); 19 | } 20 | for (int n = 0; n < 20; ++n) 21 | { 22 | for (int i = 0; i < MAXN; ++i) 23 | { 24 | d[i] += a[i] * b[i]; 25 | } 26 | } 27 | clock_t start, end; 28 | start = clock(); 29 | for (int n = 0; n < 20; ++n) 30 | { 31 | /* 可以修改的代码区域 */ 32 | // ----------------------------------- 33 | for (int i = 0; i < MAXN; ++i) 34 | { 35 | c[i] += a[i] * b[i]; 36 | } 37 | // ----------------------------------- 38 | } 39 | end = clock(); 40 | printf("time=%f\n", (double)(end - start) / CLOCKS_PER_SEC); 41 | for (int i = 0; i < MAXN; ++i) 42 | { 43 | if (fabs(c[i] - d[i]) / d[i] > 0.0001) 44 | { 45 | printf("Check Failed at %d\n", i); 46 | return 0; 47 | } 48 | } 49 | printf("Check Passed"); 50 | } -------------------------------------------------------------------------------- /docs/Lab2.5-Vectors-Bonus/index.md: -------------------------------------------------------------------------------- 1 | # 实验二 Bonus:手写 SIMD 向量化 2 | 3 | 本部分选做,感兴趣的同学可以尝试着完成。 4 | 5 | Bonus 部分完成即有加分(完成 Bonus 部分实验要求,且加速比大于 1),我们将根据完成质量提供 5-10 分的加分(与 Lab2 权重相同)。 6 | 7 | ## 1 实验环境 8 | 9 | [aistation 平台](https://aistation.zju.edu.cn:32206) 10 | 11 | ## 2 实验基础知识 12 | 13 | 现代处理器一般都支持向量化指令,x86 架构下 Intel 和 AMD 两家的处理器都提供了诸如 SSE,AVX 等 SIMD 指令集,一条指令可以同时操作多个数据进行运算,大大提高了现代处理器的数据吞吐量。 14 | 15 | 现代编译器在高优化等级下,具有自动向量化的功能,对于结构清晰,循环边界清晰的程序,编译器的自动向量化已经可以达到很优秀的程度了。然而,编译器的优化始终是保守的,很多情况下编译器无法完成使用 SIMD 指令进行向量化的工作,为了追求性能,高性能计算领域经常需要手写 SIMD 代码进行代码优化。 16 | 17 | 显然直接手写汇编指令过于困难,在 C 语言环境下,Intel 提供了一整套关于 SIMD 指令的函数封装接口和指令相关行为的参照手册,可以在实验文档的参考资料中找到。 18 | 19 | 使用这些函数 API 需要 include 对应的头文件,不同 SIMD 指令集需要的头文件不同,具体需要参考 Intel 相关文档。 20 | 21 | ```c 22 | #include 23 | #include 24 | #include 25 | ``` 26 | 27 | 另外深入到这个级别的优化已经开始需要考虑具体处理器的体系结构细节了,如某个架构下某条指令的实现延时和吞吐量是多少,处理器提供了多少向量寄存器,访存的对齐等等。这种时候编译器具体产生的汇编代码能比 C 语言代码提供更多的信息,你能了解到自己使用了多少寄存器,编译器是否生成了预期外的代码等等。 28 | 29 | 参考资料中提供的 godbolt 是一款基于 web 的研究不同编译器编译产生汇编代码的工具,大家在进行本实验的时候可以学习使用。 30 | 31 | ## 3 实验步骤 32 | 33 | ```c 34 | /* 可以修改的代码区域 */ 35 | // ----------------------------------- 36 | for (int i = 0; i < MAXN; ++i) 37 | { 38 | c[i] += a[i] * b[i]; 39 | } 40 | // ----------------------------------- 41 | ``` 42 | 43 | 需要完成的任务非常简单,将本循环使用手写 SIMD 向量化的方式进行优化。(因为是作为手写 SIMD 向量化的例子进行了简化,不接受任何其它的优化方式,优化的过程中需要保证总计算量不变且结果正确) 44 | 45 | 在编译时添加以下选项可以允许编译器生成使用 AVX2 和 FMA 指令集的代码,如果你使用了其它不在编译器默认范围内的指令集,类似的编译选项是必要的。 46 | 47 | ``` 48 | -mavx2 -mfma 49 | ``` 50 | 51 | 参照 Intel 文档优化完成后就可以开始测试优化前和优化后的性能差距,最后对比前后编译器产生的汇编代码的不同即可完成 Bonus 部分的实验。 52 | 53 | ## 4 实验初始代码 54 | 55 | 详见 [add.cpp](https://github.com/ZJUSCT/HPC101-Labs-2022/blob/main/docs/Lab2.5-Vectors-Bonus/add.cpp) 56 | 57 | ## 5 实验任务与要求 58 | 59 | 1. 完成以上简单代码的手写 SIMD 向量化 60 | 2. 测试实现的正确性和加速比 61 | 3. 提交代码和简要的思路,对比基础版本和 SIMD 版本编译器生成的汇编代码 62 | 63 | ## 参考资料 64 | 65 | - Intel® Intrinsics Guide: [https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html](https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html) 66 | - Compiler Explorer:[https://godbolt.org/](https://godbolt.org/) 67 | -------------------------------------------------------------------------------- /docs/Lab3-Cuda/img/API.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZJUSCT/HPC101-Labs-2022/c3651b7fc444a9f064bc5273231502a73cf7aaa3/docs/Lab3-Cuda/img/API.png -------------------------------------------------------------------------------- /docs/Lab3-Cuda/img/block_part.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZJUSCT/HPC101-Labs-2022/c3651b7fc444a9f064bc5273231502a73cf7aaa3/docs/Lab3-Cuda/img/block_part.png -------------------------------------------------------------------------------- /docs/Lab3-Cuda/img/conv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZJUSCT/HPC101-Labs-2022/c3651b7fc444a9f064bc5273231502a73cf7aaa3/docs/Lab3-Cuda/img/conv.png -------------------------------------------------------------------------------- /docs/Lab3-Cuda/img/env_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZJUSCT/HPC101-Labs-2022/c3651b7fc444a9f064bc5273231502a73cf7aaa3/docs/Lab3-Cuda/img/env_info.png -------------------------------------------------------------------------------- /docs/Lab3-Cuda/img/shared_memory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZJUSCT/HPC101-Labs-2022/c3651b7fc444a9f064bc5273231502a73cf7aaa3/docs/Lab3-Cuda/img/shared_memory.png -------------------------------------------------------------------------------- /docs/Lab3-Cuda/index.md: -------------------------------------------------------------------------------- 1 | # 实验三:CUDA使用基础 2 | 3 | ## 1 实验简介 4 | 5 | 卷积([Convolution](https://en.wikipedia.org/wiki/Convolution))是一种基本的数学运算,想必大家在微积分、概率论与数理统计等数学基础课程中都一定程度上接触过。作为一种基本的数学计算,其在图像处理、机器学习等领域都有重要应用。 6 | 7 | 本次实验需要你使用 CUDA 完成一个 GPU 上的二维离散卷积。 8 | 9 | 你可以自由选择使用 CUDA Runtime API 或者 CUDA Driver API 进行编程,但不能调用高性能计算的Library代替你自己实现卷积。本实验推荐采用 CUDA Runtime API,使用更加简单方便,相较Driver几乎不损失性能。 10 | 11 | ![API](img/API.png) 12 | 13 | ## 2 实验环境 14 | 15 | ### 2.0 警告 16 | 17 | 由于登录节点(H248)配置很低,禁止在上面使用 vscode-remote 等大量消耗资源的程序 18 | 19 | ### 2.1 编译器加载 20 | 21 | ``` 22 | . /opt/spack/share/spack/setup-env.sh 23 | spack load cuda@11.5.0 # 当然,你也可以选择别的 cuda 版本 24 | ``` 25 | 26 | ### 2.2 运行 27 | 28 | 实验环境与 Lab 4 一致,请使用 GPU 这个 Partition,为防止看到的 GPU 数量不正常,请使用下列命令申请节点: 29 | 30 | ``` bash 31 | srun -p GPU -N 1 --gres=gpu:1 --cpus-per-task=16 --pty bash 32 | ``` 33 | 34 | ### 2.3 Profile 35 | 36 | * Nsight Compute 在 /opt/NVIDIA-Nsight-Compute-2022.2 下 37 | * Nsight System 在 `spack load cudnn` 后直接使用 `nsys` 即可 38 | 39 | ## 3 实验基础知识介绍 40 | 41 | 该部分简要介绍和实验相关的基础知识,为方便理解,不保证数学上的严谨性。 42 | 43 | ### 3.1 张量(tensor) 44 | 45 | > 张量概念是矢量概念的推广,矢量是一阶张量。张量是一个可用来表示在一些矢量、标量和其他张量之间的线性关系的多线性函数。 46 | > 47 | > 同构意义下,第零阶张量(r = 0)为标量(Scalar),第一阶张量(r = 1)为向量 (Vector),第二阶张量(r = 2)则为矩阵(Matrix)。 48 | 49 | 实验中的卷积运算本实验涉及两个四维张量的运算。 50 | 51 | 52 | ### 3.2 卷积(convolution) 53 | 54 | 本实验只涉及离散运算,连续形式的卷积不做介绍,感兴趣的同学可以自行了解。 55 | 56 | #### 3.2.1 一维离散卷积 57 | 58 | 定义 $\left(f*g\right)\left(n\right)$ 为函数 $f$ 与 $g$ 的卷积 59 | 60 | $$ 61 | \left(f*g\right)\left(n\right)=\Sigma_{t=-\infty}^{+\infty}f\left(t\right)g\left(n-t\right) 62 | $$ 63 | 64 | 函数 $f$ 和 $g$ 定义域可以不是所有整数,修改上式中 $t$ 的遍历范围可得到新的定义;另一种方式是定义超出定义域的函数值视为 0 ,可得到相同的结果。 65 | 66 | 需要注意的是,两个函数的卷积结果仍是函数。 67 | 68 | 可以形象地理解为沿着不断移动的 $x+y=n$ 直线,将两个函数卷成一个新的函数,每条直线对应新函数的一组对应关系。 69 | 70 | #### 3.2.2 二维离散卷积 71 | 72 | 二维离散卷积可以视为一维离散卷积的推广。 73 | 74 | $$ 75 | \left(f*g\right)\left(n,m\right)=\Sigma_{i=-\infty}^{+\infty}\Sigma_{j=-\infty}^{+\infty}f\left(i,j\right)g\left(n-i,m-j\right) 76 | $$ 77 | 78 | 我们在实验中的定义卷积与数学上的定义存在差别,我们认为其在广义上属于二维离散卷积。 79 | 80 | 简化起见,考虑两个方阵 $f$ 和 $g$,$f$ 的大小为 $a*a$,$g$ 的大小为 $b*b$,我们将 $g$ 称为核(kernel)函数,且要求 $b$ 为奇数。$f$ 行列下标均从 0 开始, 81 | $g$ 的行列下标则从 $-\lfloor b/2\rfloor$ 到 $+\lfloor b/2\rfloor$ (包括0) ,此时卷积的结果可以定义为: 82 | $$ 83 | \left(f*g\right)\left(n,m\right)=\Sigma_{i=-\lfloor b/2\rfloor}^{+\lfloor b/2\rfloor}\Sigma_{j=-\lfloor b/2\rfloor}^{+\lfloor b/2\rfloor}f\left(n+i,m+j\right)g\left(i,j\right) 84 | $$ 85 | 86 | 若 $f$ 的下标范围超出定义范围,本实验的方式是填充一个默认值 (0) 以解决问题,卷积结果与$f$大小相同。 87 | 88 | ### 3.3 Bank 89 | 90 | Bank 的概念在不同种类的存储器中都有涉及,其是为了解决存储器并行访问的问题而提出的。以一个具有4个 bank 的存储器为例,我们往常在编程时认为逻辑上认为连续的内存在4个 bank 中的物理存储方式如下图所示: 91 | 92 | ``` 93 | Bank 0 Bank 1 Bank 2 Bank 3 94 | 95 | MEM[0] MEM[1] MEM[2] MEM[3] 96 | MEM[4] MEM[5] MEM[6] MEM[7] 97 | MEM[8] MEM[9] MEM[10] MEM[11] 98 | ... ... ... ... 99 | ``` 100 | 101 | 于是在同一时间我们访问诸如 `MEM[0], MEM[9], MEM[6], MEM[3]` 的存储空间就不会产生冲突,大大提高了程序的效率;否则,最差的情况下,若连续的访存序列均位于同一 bank,则效率等于串行的 4 次存储访问。 102 | 103 | 需要注意的是,若存储器的 bank 进行过针对性的优化,多个线程访问同一 bank 的同一位置可以通过同时向所有线程广播数据进行解决,同样不会产生 bank conflict 问题。 104 | 105 | 106 | 107 | ## 4 实验步骤 108 | 109 | ### 4.1 基准代码 110 | 111 | 在实际的卷积计算中,一次会进行多批(batch)的处理,比如一次处理多张图片(HxW大小)。以及同一个坐标具有多通道(channel)值,比如图片里的R、G、B三通道。`batch_size`和`in_channel`、`out_channel`定义于代码的开头。 112 | 113 | > `in_channel`即为输入的通道数,Filter(多通道的卷积核)的`in_channel`需要和输入保持一致。每个 Filter 与输入产生一个二维的输出。`out_channel`即为输出的通道数,其值实际上就是 Filter 的数量,`out_channel`个 Filter 与输入进行卷积运算,产生`out_channel`个通道的结果。 114 | > 115 | > ![conv](img/conv.png) 116 | > 117 | > 图片上经过卷积计算,输出的尺寸变小了,而我们的实验中是为输入加上了值为0的 padding ,所以输入和输出的二维尺寸是一致的。 118 | 119 | 代码中的注释和变量名遵循以下习惯: 120 | 121 | - 输入输出张量的尺寸: `size`, `H`, `W` 122 | - 输入、输出张量的批(batch)的大小: `batch_size`, `N` 123 | - 输入张量的通道数: `in_channel`, `CI` 124 | - 输出张量的通道数: `out_channel`, `CO` 125 | - 卷积核的尺寸: `kernel`, `KH`, `KW` 126 | 127 | 我们看到,卷积运算中涉及的三个张量都是四维的,我们规定它们的形状分别为: 128 | 129 | - Input: `N x H x W x CI` 130 | - Kernel: `KH x KW x CI x CO` 131 | - Output: `N x H x W x CO` 132 | 133 | 在上述二维矩阵的二维离散卷积的数学表达式基础上,我们添加批和通道两个维度,得到本次实验最终二维卷积的表达式如下: 134 | 135 | $$ 136 | \left(f*g\right)\left(n,x,y,co\right)=\sum_{i=-\lfloor KH/2\rfloor}^{\lfloor KH/2\rfloor}\sum_{j=-\lfloor KW/2\rfloor}^{\lfloor KW/2\rfloor}\sum_{ci=0}^{CI}f\left(n,x+i,y+j,ci\right)g\left(i,j,ci,co\right) 137 | $$ 138 | 139 | 二维卷积计算的 CPU 版本已在 `conv.cu` 中的`conv2d_cpu_kernel`给出,用以验证正确性。即通过批、输入通道、输出通道、卷积核高、卷积核宽的五层循环轮流计算结果矩阵中每个位置的值。其中做了 padding 的0填充等处理。 140 | 141 | > **注意:**由于正确性验证中用到了 OpenMP,它自动检测到的 CPU 核心数并不正确,可能会远超出 aistation 实际分配能调用的核心数,导致速度异常缓慢。因此,你需要设置环境变量: 142 | > 143 | > ```shell 144 | > export OMP_NUM_THREADS=4 145 | > ``` 146 | 147 | 基准代码为程序中的`conv2d_cuda_kernel`核函数,是未经优化的五层循环嵌套GPU实现,你可以在此基础上进行改进,亦或者重新自己实现。 148 | 149 | ```c++ linenums="1" 150 | __global__ void conv2d_cuda_kernel(const uint8_t *__restrict__ a, 151 | const uint8_t *__restrict__ w, 152 | uint8_t *__restrict__ b) 153 | { 154 | const int i = blockIdx.x * block_size + threadIdx.x; 155 | const int j = blockIdx.y * block_size + threadIdx.y; 156 | if (i < size && j < size) { 157 | for (int s = 0; s < batch_size; ++s) { 158 | for (int CO = 0; CO < out_channel; ++CO) { 159 | uint8_t conv = 0; 160 | // Conv2d for a single pixel, single output channel. 161 | for (int CI = 0; CI < in_channel; ++CI) { 162 | int x = i - kernel / 2, y = j - kernel / 2; 163 | for (int k = 0; k < kernel; ++k) { 164 | for (int l = 0; l < kernel; ++l) { 165 | if (!(x < 0 || x >= size || y < 0 || y >= size)) { 166 | conv += a(s, x, y, CI) * w(k, l, CI, CO); 167 | } 168 | y++; 169 | } 170 | x++; 171 | y -= kernel; 172 | } 173 | } 174 | // Write back to b. 175 | b(s, i, j, CO) = conv; 176 | } 177 | } 178 | } 179 | } 180 | ``` 181 | 182 | ### 4.2 Shared Memory 183 | 184 | 正如课上所讲,GPU 中有一块共享内存被同一线程块中的线程共享,在存储层级中,Shared Memory 与 L1 Cache 同级,部分 GPU 架构中还可以手动分配 L1 Cache 与 Shared Memory 的大小;利用 Shared Memory 将线程块的密集访存加速能够获得极低的访存延迟且大大节省内存带宽。 185 | 186 | shared_memory 187 | 188 | ### 4.3 Blocking 189 | 190 | 可以对大矩阵进行分块计算,提高访存局部性。这一技术在 lab4 中会详细讲述。 191 | 192 | 以下是矩阵乘法的分块示意图,卷积优化思路可以参考矩阵乘法分块思路。 193 | 194 | block_optimization 195 | 196 | ### 4.4 Virtual Thread Split 197 | 198 | 重新组织线程的编号方式与执行顺序(自由发挥),尽可能的防止 bank conflict,最大化利用显存带宽。 199 | 200 | 为了提高线程读写带宽,GPU 中的共享内存会被划分成若干个 bank,理想状况下,各个线程同一时间访问的 bank 应该是不同的。 201 | 202 | ### 4.5 Cooperative Fetching 203 | 204 | 为了减少单个线程的内存访问量,可以让每个线程块中的线程合作访问有共同依赖的部分;共享内存是有限的,将访存重叠度高的线程安排在单个线程块中,从全局内存中加载访问更密集的数据到共享内存,都可以提升程序效率。 205 | 206 | ### 4.6 Hint & Bonus 207 | 208 | 如果程序遇到难以解决的正确性问题,不妨考虑两个关键词: `sync` 和 `atomic`。 209 | 210 | 另外在我们本次实验提供的 GPU (RTX 2080Ti) 上,包含一个叫做 TensorCore 的硬件,它能够进一步加速卷积的计算, 在 Cuda 9.0 之后,你可以使用内嵌`PTX`汇编或者 CUDA 的 C++ 扩展`nvcuda::wmma`的方式 211 | 来显式地调用Tensor Core来进行计算。 212 | 213 | Tensor Core 能在一个周期内完成一个小矩阵乘法,因而提高计算效率,但是Tensor Core对作矩阵乘法的两个矩阵的形状要求比较高(例如4x4x4,8x8x8等),你需要合理地对矩阵进行切分和对 Wrap和Block 中的线程进行分配来最大化 Tensor Core 的计算性能。了解如何调用 Tensor Core,可以查阅文档尾部的参考文献。 214 | 215 | 使用 Tensor Core 完成本次实验,你将会获得 Bonus。 216 | 217 | 218 | ## 5 实验初始代码 219 | 220 | 详见 [starter_code](https://github.com/ZJUSCT/HPC101-Labs-2022/tree/main/docs/Lab3-Cuda/starter_code)。 221 | 222 | ## 6 实验任务与要求 223 | 224 | 利用以上技术(包括但不限于),在基准程序的基础上实现卷积计算的 GPU 实现并优化之。 225 | 226 | **只允许修改两个计时点(不含)之间的代码及 Makefile 文件** 227 | 228 | **可以编写任意函数,但函数的调用栈需要能够回溯到两个计时点之间** 229 | 230 | **若对不允许修改部分代码正确性有疑问请联系助教** 231 | 232 | 本实验的目的是让大家学习实践课程教授的 CUDA 优化知识,熟悉 GPU 编程与优化,掌握面对常见并行问题的调试技巧。**不允许使用cuDNN等算子库或者使用第三方工具自动生成的代码**。 233 | 234 | > **Note**: 调试时为使错误可复现,可以将代码中的 `std::default_random_engine generator(r());` 改为 `std::default_random_engine generator;`,这样每次生成的随机矩阵都会是一致的。 235 | 236 | ## 7 评价标准 237 | 238 | 若参考互联网资料或者代码请在报告中注明出处。 239 | 240 | **注意:参考和复制粘贴改变量名是完全两回事!!!** 241 | 242 | 1. 只要完成 CUDA 代码的一定优化且得到正确结果,就能取得大部分分数。 243 | 2. 如果优化结果优异,直接满分(**你有更好的想法,我们鼓励尝试**)。 244 | 3. 优化结果普通,我们将参考你对实验手册中提到的优化策略的尝试与努力(报告与代码)进行给分——若你已经尽力尝试了手册中所有的优化思路,你可以取得(95+)的分数。 245 | 246 | 请让我们看到你的尝试,即使代码不能运行或者结果错误也不要羞涩于提交(否则实在捞不起来)! 247 | 248 | ## 参考文献 249 | 250 | - NVIDIA [Convolutional Layers User's Guide](https://docs.nvidia.com/deeplearning/performance/dl-performance-convolutional/) 251 | - NVIDIA Developer Blog [Tips for Optimizing GPU Performance Using Tensor Cores](https://developer.nvidia.com/blogoptimizing-gpu-performance-tensor-cores/) 252 | - `nvcuda::wmma` CUDA C++ Extension [NVIDIA CUDA C Programming Guide](https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#wmma) 253 | - Parallel Thread Execution [NVIDIA PTX ISA](https://docs.nvidia.com/cuda/parallel-thread-execution/index.html) 254 | -------------------------------------------------------------------------------- /docs/Lab3-Cuda/starter_code/Makefile: -------------------------------------------------------------------------------- 1 | TARGET = conv 2 | SOURCE = conv.cu 3 | 4 | NVCC = nvcc 5 | NVCCFLAGS += -O3 -cudart=shared -Xcompiler -fopenmp 6 | 7 | $(TARGET):$(SOURCE) 8 | $(NVCC) $(SOURCE) -o $(TARGET) $(NVCCFLAGS) 9 | 10 | .PHONY:clean 11 | clean: 12 | rm -rf $(TARGET) -------------------------------------------------------------------------------- /docs/Lab3-Cuda/starter_code/conv.cu: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | const int alignment = 32; // 32 byte alignment 8 | const int size = 100; 9 | const int kernel = 3; // odd 10 | const int batch_size = 128; 11 | const int in_channel = 128; 12 | const int out_channel = 128; 13 | 14 | #define InitRandom() \ 15 | std::random_device r; \ 16 | std::default_random_engine generator(r()); \ 17 | std::uniform_int_distribution<> distribution(0, 255); 18 | 19 | #define a(_n, _x, _y, _c) a[(_n) * size * size * in_channel + (_x) * size * in_channel + (_y) * in_channel + (_c)] 20 | #define w(_x, _y, _ci, _co) w[(_x) * kernel * in_channel * out_channel + (_y) * in_channel * out_channel + (_ci) * out_channel + (_co)] 21 | #define b(_n, _x, _y, _c) b[(_n) * size * size * out_channel + (_x) * size * out_channel + (_y) * out_channel + (_c)] 22 | #define CUDA_CALL(func) \ 23 | { \ 24 | cudaError_t e = (func); \ 25 | if (!(e == cudaSuccess || e == cudaErrorCudartUnloading)) \ 26 | { \ 27 | fprintf(stderr, "CUDA: %s\n", cudaGetErrorString(e)); \ 28 | abort(); \ 29 | } \ 30 | } 31 | 32 | 33 | /// \brief Generate [N, H, W, C] input tensor and [H, W, I, O] kernel tensor. 34 | void Generate(uint8_t *const a, uint8_t *const w) { 35 | #pragma omp parallel for 36 | // Batch dimension. 37 | for (int s = 0; s < batch_size; ++s) { 38 | InitRandom(); 39 | // Height dimension. 40 | for (int i = 0; i < size; ++i) 41 | // Width dimension. 42 | for (int j = 0; j < size; ++j) { 43 | const int channel_lower = s * size * size * in_channel 44 | + i * size * in_channel 45 | + j * in_channel; 46 | const int channel_upper = channel_lower + in_channel; 47 | // Channel dimension. 48 | for (int c = channel_lower; c < channel_upper; ++c) 49 | a[c] = distribution(generator); 50 | } 51 | } 52 | #pragma omp parallel for 53 | for (int i = 0; i < kernel; ++i) { 54 | InitRandom(); 55 | for (int j = 0; j < kernel; ++j) 56 | for (int CI = 0; CI < in_channel; ++CI) { 57 | const int channel_lower = i * kernel * in_channel * out_channel 58 | + j * in_channel * out_channel 59 | + CI * out_channel; 60 | const int channel_upper = channel_lower + out_channel; 61 | for (int CO = channel_lower; CO < channel_upper; ++CO) 62 | w[CO] = distribution(generator); 63 | } 64 | } 65 | } 66 | 67 | void conv2d_cpu_kernel(const uint8_t *__restrict__ a, 68 | const uint8_t *__restrict__ w, 69 | uint8_t *__restrict__ b) { 70 | #pragma omp parallel for 71 | for (int s = 0; s < batch_size; ++s) { 72 | size_t output_bytes = ((out_channel * sizeof(uint8_t)) + (size_t)alignment - 1) & ~((size_t)alignment -1); 73 | uint8_t *packedB = static_cast(malloc(output_bytes)); 74 | 75 | size_t input_bytes = ((kernel * kernel * in_channel * sizeof(uint8_t)) + (size_t)alignment - 1) & ~((size_t)alignment - 1); 76 | uint8_t *packedA = static_cast(malloc(input_bytes)); 77 | 78 | for (int i = 0; i < size; ++i) 79 | for (int j = 0; j < size; ++j) { 80 | // Collected needed input data, 81 | // Start from A[s, i - kernel / 2, j - kernel / 2, 0]. 82 | int x = i - kernel / 2; 83 | int y = j - kernel / 2; 84 | int input_index = s * size * size * in_channel 85 | + x * size * in_channel 86 | + y * in_channel; 87 | memset(packedA, 0, input_bytes); 88 | int A_buffer_index = 0; 89 | for (int kh = 0; kh < kernel; ++kh) { 90 | for (int kw = 0; kw < kernel; ++ kw) { 91 | if (!(x < 0 || x >= size || y < 0 || y >= size)) { 92 | memcpy(packedA + A_buffer_index, a + input_index, in_channel * sizeof(uint8_t)); 93 | } 94 | else { 95 | memset(packedA + A_buffer_index, 0, in_channel * sizeof(uint8_t)); 96 | } 97 | y++; 98 | A_buffer_index += in_channel; 99 | input_index += in_channel; 100 | } 101 | x++; 102 | y -= kernel; 103 | input_index = input_index - kernel * in_channel + size * in_channel; 104 | } 105 | 106 | // Start from B[s, i, j, 0] 107 | int output_index = s * size * size * out_channel 108 | + i * size * out_channel 109 | + j * out_channel; 110 | memset(packedB, 0, output_bytes); 111 | 112 | // Start from W[0, 0, 0, 0] 113 | int kernel_index = 0; 114 | A_buffer_index = 0; 115 | // Convolution 2D computation. 116 | // iterate over each in_channel of input tensor, 117 | // and accumulate contribution to output tensor. 118 | for (int N = 0; N < kernel * kernel; ++N) { 119 | for (int CI = 0; CI < in_channel; ++CI) { 120 | for (int CO = 0; CO < out_channel; ++CO) { 121 | packedB[CO] += packedA[A_buffer_index] * w[kernel_index]; 122 | kernel_index++; // move to next output channel. 123 | } 124 | A_buffer_index++; 125 | } 126 | } 127 | memcpy(b + output_index, packedB, sizeof(uint8_t) * out_channel); 128 | } 129 | free(packedA); 130 | free(packedB); 131 | } 132 | } 133 | 134 | void Check(const uint8_t *const a, const uint8_t *const w, uint8_t *const b) { 135 | auto b_std = new uint8_t[batch_size * size * size * out_channel]; 136 | std::cout << "Conv2d CPU Kernel Start... \n"; 137 | conv2d_cpu_kernel(a, w, b_std); 138 | std::cout << "Checking Results... \n"; 139 | size_t N = batch_size * size * size * out_channel; 140 | for (size_t i = 0; i < N; ++i) { 141 | if (b[i] != b_std[i]) { 142 | std::cout << "\x1b[31m" 143 | "Wrong Answer" 144 | "\x1b[0m" 145 | " at " 146 | << i << std::endl; 147 | std::cout << "expected " << (int)b_std[i] << " but found " << (int)b[i] 148 | << std::endl; 149 | delete[] b_std; 150 | return; 151 | } 152 | } 153 | std::cout << "\x1b[32m" 154 | "Correct" 155 | "\x1b[0m" 156 | << std::endl; 157 | 158 | delete[] b_std; 159 | } 160 | 161 | const int block_size = 16; 162 | /// \brief Do Conv2d with NHWC Input with HWIO Kernel, and NHWC output 163 | __global__ void conv2d_cuda_kernel(const uint8_t *__restrict__ a, 164 | const uint8_t *__restrict__ w, 165 | uint8_t *__restrict__ b) 166 | { 167 | const int i = blockIdx.x * block_size + threadIdx.x; 168 | const int j = blockIdx.y * block_size + threadIdx.y; 169 | if (i < size && j < size) { 170 | for (int s = 0; s < batch_size; ++s) { 171 | for (int CO = 0; CO < out_channel; ++CO) { 172 | uint8_t conv = 0; 173 | // Conv2d for a single pixel, single output channel. 174 | for (int CI = 0; CI < in_channel; ++CI) { 175 | int x = i - kernel / 2, y = j - kernel / 2; 176 | for (int k = 0; k < kernel; ++k) { 177 | for (int l = 0; l < kernel; ++l) { 178 | if (!(x < 0 || x >= size || y < 0 || y >= size)) { 179 | conv += a(s, x, y, CI) * w(k, l, CI, CO); 180 | } 181 | y++; 182 | } 183 | x++; 184 | y -= kernel; 185 | } 186 | } 187 | // Write back to b. 188 | b(s, i, j, CO) = conv; 189 | } 190 | } 191 | } 192 | } 193 | 194 | // naive and shit 195 | // only for testing correctness and precision 196 | void conv_cuda(const uint8_t *const a, const uint8_t *const w, uint8_t *const b, 197 | cudaEvent_t *start_e, cudaEvent_t *stop_e) 198 | { 199 | uint8_t *a_kernel, *w_kernel, *b_kernel; 200 | CUDA_CALL(cudaMalloc(&a_kernel, batch_size * size * size * in_channel * sizeof(uint8_t))); 201 | CUDA_CALL(cudaMemcpy(a_kernel, a, batch_size * size * size * in_channel * sizeof(uint8_t), 202 | cudaMemcpyHostToDevice)); 203 | CUDA_CALL(cudaMalloc(&w_kernel, kernel * kernel * in_channel * out_channel * sizeof(uint8_t))); 204 | CUDA_CALL(cudaMemcpy(w_kernel, w, kernel * kernel * in_channel * out_channel * sizeof(uint8_t), 205 | cudaMemcpyHostToDevice)); 206 | CUDA_CALL(cudaMalloc(&b_kernel, batch_size * size * size * out_channel * sizeof(uint8_t))); 207 | // Start Timer. 208 | cudaEventRecord(*start_e); 209 | // Run Conv2d Kernel, 210 | // Timer for computation cuda kernel. 211 | dim3 grid((size + block_size - 1) / block_size, 212 | (size + block_size - 1) / block_size); 213 | dim3 block(block_size, block_size); 214 | // @note: you can also use CUDA API to launch a cuda kernel function, 215 | // __host__ cudaError_t cudaLaunchKernel; 216 | conv2d_cuda_kernel<<>>(a_kernel, w_kernel, b_kernel); 217 | cudaError_t kernel_err = cudaGetLastError(); 218 | if (kernel_err != cudaSuccess) { 219 | printf("CUDA Kernel: %s", cudaGetErrorString(kernel_err)); 220 | abort(); 221 | } 222 | cudaDeviceSynchronize(); 223 | // Stop Timer 224 | cudaEventRecord(*stop_e); 225 | cudaEventSynchronize(*stop_e); 226 | 227 | CUDA_CALL(cudaMemcpy(b, b_kernel, batch_size * size * size * out_channel * sizeof(uint8_t), 228 | cudaMemcpyDeviceToHost)); 229 | cudaFree(a_kernel); 230 | cudaFree(w_kernel); 231 | cudaFree(b_kernel); 232 | } 233 | 234 | int main() { 235 | auto a = new uint8_t[batch_size * size * size * in_channel]; 236 | auto w = new uint8_t[kernel * kernel * in_channel * out_channel]; 237 | auto b = new uint8_t[batch_size * size * size * out_channel]; 238 | std::cout << "Generating input and kernel tensor... \n"; 239 | Generate(a, w); 240 | 241 | cudaEvent_t start_e, stop_e; 242 | cudaEventCreate(&start_e); 243 | cudaEventCreate(&stop_e); 244 | 245 | // Conv(a, w, b); 246 | std::cout << "Conv2d Cuda Kernel Start... \n"; 247 | conv_cuda(a, w, b, &start_e, &stop_e); 248 | 249 | std::cout << "Verifying... \n"; 250 | Check(a, w, b); 251 | float milliseconds = 0; 252 | cudaEventElapsedTime(&milliseconds, start_e, stop_e); 253 | std::cout << milliseconds << " milliseconds" << std::endl; 254 | cudaEventDestroy(start_e); 255 | cudaEventDestroy(stop_e); 256 | 257 | // Output(a, w, b); 258 | delete[] a; 259 | delete[] w; 260 | delete[] b; 261 | return 0; 262 | } 263 | -------------------------------------------------------------------------------- /docs/Lab4-Gemm/code/Makefile: -------------------------------------------------------------------------------- 1 | CC=gcc 2 | CFLAGS=-mcmodel=medium 3 | # compiler may crash when static array too large, 4 | # add `-mcmodel=medium` in this case. 5 | 6 | all: 7 | $(CC) -o gemm hw_baseline.cpp $(CFLAGS) 8 | 9 | .PHONY: run 10 | run: all 11 | ./gemm 12 | 13 | .PHONY: clean 14 | clean: 15 | rm -rf *.o gemm -------------------------------------------------------------------------------- /docs/Lab4-Gemm/code/README.md: -------------------------------------------------------------------------------- 1 | # Build 2 | 3 | ```bash 4 | $ make 5 | ``` 6 | 7 | # Run 8 | 9 | ```bash 10 | $ make run 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/Lab4-Gemm/code/hw.h: -------------------------------------------------------------------------------- 1 | // original author: Xu Qiyuan 2 | #pragma once 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define __X_N 10001 10 | int __X_matA[__X_N*__X_N], __X_matB[__X_N*__X_N], 11 | __X_tv_m[__X_N], __X_tv_m2[__X_N]; 12 | struct timespec __X_begin, __X_end; 13 | 14 | void input(int* matA, int* matB) { 15 | static int pid = -1; 16 | if (pid != -1) { 17 | puts("input can only be called once by one process/thread"); 18 | abort(); 19 | } 20 | pid = 0; 21 | 22 | srand(time(NULL)); 23 | #pragma omp parallel for 24 | for (int i=0;i<__X_N*__X_N;++i) 25 | __X_matA[i] = rand(); 26 | #pragma omp parallel for 27 | for (int i=0;i<__X_N*__X_N;++i) 28 | __X_matB[i] = rand(); 29 | #pragma omp parallel for 30 | for (int i = 0; i < __X_N ; ++i) 31 | for (int j = 0; j < __X_N ; ++j) 32 | matA[i*__X_N + j] = __X_matA[j*__X_N + i]; 33 | #pragma omp parallel for 34 | for (int i = 0; i < __X_N ; ++i) 35 | for (int j = 0; j < __X_N ; ++j) 36 | matB[i*__X_N + j] = __X_matB[j*__X_N + i]; 37 | clock_gettime(CLOCK_MONOTONIC, &__X_begin); 38 | } 39 | 40 | void output(int* result, int n) { 41 | clock_gettime(CLOCK_MONOTONIC, &__X_end); 42 | printf("Performance : %lf Mops\n", (double)__X_N * __X_N * (__X_N + 1) * n / 1000000/ ((double)__X_end.tv_sec - __X_begin.tv_sec + 0.000000001 * (__X_end.tv_nsec - __X_begin.tv_nsec))); 43 | srand(__X_end.tv_nsec); 44 | int test_on = rand() % __X_N, 45 | *__X_tv = __X_tv_m, *__X_tv2 = __X_tv_m2; 46 | for (int i=0;i<__X_N;++i) 47 | __X_tv[i] = __X_matA[i*__X_N + test_on]; 48 | for (int k=0;k@clusters.zju.edu.cn -p 14514` 27 | 28 | 其中 `username` 为 `{你的姓名缩写}-summer` ,例:王小明的用户名为 `wxm-summer`。 29 | 30 | 对于部分缩写重名的同学,用户名略有不同,具体请查看 [username.txt](index.assets/username.txt)。 31 | 32 | #### 2.2.2 编译 33 | 34 | 集群上已经安装好了 `Intel OneAPI` 套件,需要执行 `source /opt/intel/oneapi/setvars.sh` 加载环境,其中包含了 `IntelMPI`。 35 | 36 | 编译时请使用 `mpiicc` 或 `mpiicpc` 编译器。 37 | 38 | #### 2.2.3 运行 39 | 40 | 在加载上述 `IntelMPI` 套件之后,可以使用下面几种方式运行程序(节点数和进程数请自行选择): 41 | 42 | * 使用 `srun` 把任务提交至任务队列 43 | ```bash 44 | I_MPI_PMI_LIBRARY=/usr/lib/x86_64-linux-gnu/libpmi.so.0 srun -N 4 -n 4 ./your_program 45 | ``` 46 | 47 | * 使用 `salloc` 请求集群资源,待资源分配完毕后手动运行 48 | ```bash 49 | salloc -N 4 50 | # wait for a while 51 | mpirun -ppn 1 ./your_program 52 | ``` 53 | 54 | 单次任务的最大运行时间为 10 分钟。**在实验截止日期前一周,最大运行时间将会减少。** 55 | 56 | #### 2.2.4 集群状态获取 57 | 58 | 可以通过 `sinfo` 获取当前集群的状态,通过 `squeue` 获取排队的任务信息。如果当前自己的任务正在运行,则你可以通过 `ssh` 连接到各个计算节点通过 `htop` 等命令观察运行情况。 59 | 60 | ### 2.3 集群选择 61 | 62 | **为避免阻塞,请尽量先在 aistation 上完成编写与调试。** 63 | 64 | 65 | ## 3 实验基础知识介绍 66 | 67 | ### 3.1 程序局部性 68 | 69 | > 此部分介绍参考自 [wiki](https://en.wikipedia.org/wiki/Locality_of_reference) 70 | 71 | 程序局部性指的是应用程序在访问内存的时候,倾向于访问内存中较为靠近的值。 72 | 73 | 一般来说,程序的局部性分为两种形式,一种是时间局部性,另一种是空间局部性。时间局部性指的是,程序在运行时,最近刚刚被引用过的一个内存位置容易再次被引用,比如在调取一个函数的时候,前不久才调取过的本地参数容易再度被调取使用。空间局部性指的是,最近引用过的内存位置以及其周边的内存位置容易再次被使用。空间局部性比较常见于循环中,比如在一个数列中,如果第 3 个元素在上一个循环中使用,则本次循环中极有可能会使用第 4 个元素。 74 | 75 | 局部性是出现在计算机系统中的一种可预测行为。系统的这种强访问局部性,可以被用来在处理器内核的指令流水线中进行性能优化,如缓存,内存预读取以及分支预测。 76 | 77 | ### 3.2 计算机层次存储结构 78 | 79 | 存储层次是在计算机体系结构下存储系统层次结构的排列顺序。每一层于下一层相比都拥有较高的速度和较低延迟性,以及较小的容量。 80 | 81 | preview 82 | 83 | 层次存储的设计核心目的就是要充分利用程序的局部性,如将最常访问的数据放置在较高的层级来保证访问速度;再比如按照顺序访问能够使得每次取来的整块数据都能够被利用,也使得预取的数据是有效的;如果你的内存充足,你甚至可以将硬盘上的数据提前拷贝到内存,来避免硬盘 I/O 带来的开销,等等。 84 | 85 | 因此,充分优化一个程序的局部性,能够使得其充分利用现代计算机硬件上各种加速设计,提高其运行效率。 86 | 87 | ### 3.3 并行计算 88 | 89 | #### 3.3.1 SIMD 90 | 91 | ![img](index.assets/225px-SIMD.svg.png) 92 | 93 | SIMD 是一种数据并行技术,它通过提供支持向量运算的指令,同时对一组数据执行相同的计算,从而实现空间上的并行,进而提高程序的并行度和吞吐量。当程序中出现大量完全一致的运算需要对一批数据进行处理时,你可以考虑使用 SIMD 对其进行并行。 94 | 95 | #### 3.3.2 指令级并行 96 | 97 | pipeline 98 | 99 | 现代处理器一般都会使用流水线技术来同时执行多条指令的不同阶段,从而实现指令间的并行。传统流水线因为需要解决流水线中的各种冲突,不可避免的会在流水线中带来空泡,而由于现代处理器里其实还包含指令的乱序发射,出现空泡的几率已经大大降低,所以在编程时不太需要考虑这方面的问题。 100 | 101 | #### 3.3.3 线程级并行 102 | 103 | 前面介绍的并行都是对于单个物理核心的场景,在现代计算机中,我们往往拥有多个物理核心,而线程作为调度的最小单位只能在一个核心上执行,因此我们需要开启多个线程来充分利用每一个核心。 104 | 105 | 线程的一个好处是内存共享,这意味着线程间的通信开销会小不少。因此在单机上我们往往采用多线程的并行模型。不过为了保证计算的正确性,你需要确保一些地方的操作是原子的,并维护好一些同步点。 106 | 107 | #### 3.3.4 进程级并行 108 | 109 | 对于分布式的计算来说,仅仅使用线程并行已经不够了。我们需要在每个节点上开启不同的进程,来激发更多的并行能力。由于进程间的内存并不共享,我们需要进程间通信来进行数据的分享。 110 | 111 | ### 3.3 IO 与通信开销 112 | 113 | 如果程序有 IO 或者网络通信需求,而我们程序的根本需求其实是计算,所以想办法缩短这一部分的时间也是提高程序性能的重要手段:比如减少通信量,尽量将通信 / IO集中在一起,增加网络 / IO 的带宽等。 114 | 115 | 116 | 117 | ## 4 实验步骤 118 | 119 | 接下来我们讨论的优化技巧全部是针对两个稠密矩阵的乘法。我们给出以下形式化定义: 120 | 121 | 给定矩阵 $A, B, C$: 122 | 123 | $$ 124 | \mathbf{A} =\begin{pmatrix}a_{11}&a_{12}&\cdots &a_{1n}\\a_{21}&a_{22}&\cdots &a_{2n}\\\vdots &\vdots &\ddots &\vdots \\a_{m1}&a_{m2}&\cdots &a_{mn}\\\end{pmatrix}, \quad 125 | \mathbf {B} =\begin{pmatrix}b_{11}&b_{12}&\cdots &b_{1p}\\b_{21}&b_{22}&\cdots &b_{2p}\\\vdots &\vdots &\ddots &\vdots \\b_{n1}&b_{n2}&\cdots &b_{np}\\\end{pmatrix}, \quad 126 | \mathbf {C} =\begin{pmatrix}c_{11}&c_{12}&\cdots &c_{1p}\\c_{21}&c_{22}&\cdots &c_{2p}\\\vdots &\vdots &\ddots &\vdots \\c_{m1}&c_{m2}&\cdots &c_{mp}\\\end{pmatrix} 127 | $$ 128 | 129 | 矩阵乘法 $C = AB$ 定义为对任意 $c_{ij}$ 有: 130 | 131 | $$ 132 | c_{ij}=a_{i1}b_{1j}+a_{i2}b_{2j}+\cdots +a_{in}b_{nj}=\sum _{k=1}^{n}a_{ik}b_{kj} 133 | $$ 134 | 135 | 为了简化问题,我们假设所有的矩阵都是 $N \times N$ 的方阵。 136 | 137 | ### 4.1 单机优化 138 | 139 | 下面我们选择介绍一些单机上的优化技术。 140 | 141 | #### 4.1.1 基准 142 | 143 | 最基础的矩阵乘法自然是三层循环,即对二维矩阵 $$C$$ 的每一项通过单层循环计算其结果。 144 | 145 | ```c++ 146 | for (int x = 0; x < N; x++) { 147 | for (int y = 0; y < N; y++) { 148 | C[(x * N) + y] = 0 149 | for (int k = 0; k < N; k++) { 150 | C[(x * N) + y] += A[(x * N) + k] * B[(k * N) + y] 151 | } 152 | } 153 | } 154 | ``` 155 | 156 | #### 4.1.2 分块 157 | 158 | 基准代码尽可能多的使用了行遍历来提高内存的访问效率,但是即便如此,由于矩阵本身大小过大,导致部分按列访问的情况下,整体局部性还是不高。我们引入分块技术来进一步提高程序的局部性,降低 cache miss 的概率。 159 | 160 | $$ 161 | A=\left(\begin{array}{ccc} 162 | A_{0,0} & \cdots & A_{0, K-1} \\ 163 | \vdots & & \vdots \\ 164 | A_{M-1,0} & \cdots & A_{M-1, K-1} 165 | \end{array}\right), \quad B=\left(\begin{array}{ccc} 166 | B_{0,0} & \cdots & B_{0, N-1} \\ 167 | \vdots & & \vdots \\ 168 | B_{K-1,0} & \cdots & B_{K-1, N-1} 169 | \end{array}\right), \quad C=\left(\begin{array}{ccc} 170 | C_{0,0} & \cdots & C_{0, N-1} \\ 171 | \vdots & & \vdots \\ 172 | C_{M-1,0} & \cdots & C_{M-1, N-1} 173 | \end{array}\right) 174 | $$ 175 | 176 | 此时对于每一个 $C$,我们都有:$C_{ij}=\sum _{k=0}^{N-1}A_{ik}B_{kj}$ 177 | 大致代码实现如下: 178 | 179 | ```c++ 180 | for (int outerX = 0; outerX < N; outerX++) { 181 | for (int outerY = 0; outerY < N; outerY++) { 182 | // Clear C_ij matrix 183 | for (int innerX = 0; innerX < blockSize; innerX++) { 184 | for (int innerY = 0; innerY < blockSize; innerY++) { 185 | // TODO: Clear C_ij matrix 186 | } 187 | } 188 | for (int K = 0; K < N; K++) { 189 | // TODO: calculate C_ij = sum(A_ik * B_kj) 190 | } 191 | } 192 | } 193 | ``` 194 | 195 | 为了保证计算的正确性,你需要非常谨慎的处理循环层数、下标。 196 | 197 | #### 4.1.3 向量化 198 | 199 | 对于循环的最内层,如果我们将其展开,往往会发现类似下面的模式: 200 | 201 | ```c++ 202 | c[i+0] = a[j+0] * b[someIdx0] 203 | c[i+1] = a[j+1] * b[someIdx1] 204 | c[i+2] = a[j+2] * b[someIdx2] 205 | c[i+3] = a[j+3] * b[someIdx3] 206 | ... 207 | ``` 208 | 209 | 这种规律、统一的计算非常适合使用 SIMD 指令进行计算。以计算 $C_i = A_i * B_i$ 为例,我们可以写出以下利用 AVX 指令的向量化代码: 210 | 211 | ```c++ 212 | __m256d a[N], b[N], c[N]; 213 | for (int i = 0; i < N; i++) 214 | c[i] = __mm256_mul_pd(a[i], b[i]); 215 | ``` 216 | 217 | 这段代码能够在一条 CPU 指令内完成 4 个 double 的乘法,从而大幅提高系统的计算性能。 218 | 219 | 同样,为了保证计算的正确性,你需要非常谨慎的处理循环层数、地址。 220 | 221 | 由于向量化指令集在不同厂商、不同型号的 CPU 上可能都是不同的,手写向量化的成本非常的高,因此我们往往**直接使用编译器的自动向量化**。具体开启方法请查阅不同平台上相关编译器的编译选项。 222 | 223 | #### 4.1.4 数组封装 224 | 225 | 数组封装是 GEMM 中的一个重要技术。简单来说就是即便使用了分块技术,B 中的访存顺序依然是不太连贯的——以 $16 \times 16$ 大小的矩阵为例,即便我们按照 $4 \times 4$ 进行分块,B 中的访存顺序仍然会是每 4 次线性访问遇到一次非线性访问,如下图上半部分所示。 226 | 227 | ![img](index.assets/array-packing.png) 228 | 229 | 解决方案是:提前将 B 矩阵拆分成 4 个小矩阵,每个矩阵大小为 $16 \times 4$。拆分后的每个矩阵内部是连续存放的,这样所有的访存操作都是顺序的了,如图中下半部分所示。 230 | 231 | #### 4.1.5 写入缓存 232 | 233 | 在之前体系结构概述的课程里,我们已经介绍过写入内存的时候,也是存在局部性的。虽然存在不同的缓存替换、写回策略,但是总体上顺序写入仍然是远远快于非线性的写入的。因此我们可以先创建一块写入缓存,用于按照计算顺序存放结果,再在计算全部结束后批量拷贝、调整到应有的顺序。 234 | 235 | #### 4.1.6 线程级并行 236 | 237 | 在优化完局部性后,我们开始考虑使用线程级并行。这里我们使用 OpenMP 即可,具体方法课上已经介绍。请注意线程间的负载均衡,并平衡调度开销。 238 | 239 | 多线程里需要注意的一点是,由于 NUMA 的存在,跨片访问的开销将会非常的大,因此提高核的数据亲和性也非常重要,如将线程绑定到指定核心上,避免被 OS 调度到其他核心,以及一个核上的线程应当尽量只访问该核心所直连的内存区域。 240 | 241 | ### 4.2 分布式并行 242 | 243 | 单机优化完毕后,再考虑分布式系统的优化。我们使用 MPI 来处理线程间的通信,具体方法课上已经介绍。由于进程间内存空间是不共享的,计算所需的数据需要手工进行分发,你需要合理设计通信方式,避免过度的通信开销。另外,也不要忽略了同步开销。 244 | 245 | > 更多优化技巧可以参阅手册末尾的参考资料 246 | 247 | 248 | 249 | ## 5 实验任务与要求 250 | 251 | 利用以上技术完成 GEMM 分布式实现,用于完成以下计算任务: 252 | 253 | ### 5.1 问题描述 254 | 255 | 输入矩阵 $A, B$,标量 $n$,计算矩阵连乘积: 256 | $$ 257 | \prod_{k=0}^nA+kB = A (A + B) (A+2B)\dots(A+nB) 258 | $$ 259 | 其中矩阵 $A, B$ 为随机生成的 $10001 \times 10001$ `signed int` 32 矩阵。出于方便起见无需考虑溢出。 260 | 261 | ### 5.2 评测环境 262 | 263 | `slurm` 集群,详见本手册第 2 节。 264 | 265 | ### 5.3 基准代码 266 | 267 | 我们将提供一份代码包(详见 [starter_code](https://github.com/ZJUSCT/HPC101-Labs-2022/tree/main/docs/Lab4-Gemm/code)),包含正确性检验以及性能计算,形式如下。 268 | 269 | `hw.h` 270 | 271 | ```c++ 272 | // mat_A, mat_B and result point to memory at least 10001*10001*4B 273 | // n is the scalar and will be used to check your answer 274 | void input(int* mat_A, int* mat_B); 275 | void output(int* result, int n); 276 | ``` 277 | 278 | `hw_baseline.cpp` 279 | 280 | ```c++ 281 | #include "hw.h" 282 | 283 | int matA[10001*10001]; 284 | int matB[10001*10001]; 285 | 286 | int main() { 287 | input(matA, matB); 288 | int n = 1; 289 | // do calculation here 290 | output(matA, n); 291 | } 292 | ``` 293 | 294 | 你可以使用 `make` 构建二进制程序,并使用 `make run` 运行你的二进制程序。 295 | 296 | ### 5.4 评测方式 297 | 298 | 我们使用每秒计算量 (Operations Per Second)来评价你的程序性能,其计算⽅法如下: 299 | 300 | $$ 301 | Ops = \frac{n}{T}C 302 | $$ 303 | 304 | 其中 $C=10001^3+2\cdot10001^2$ 为常量。 305 | 306 | ### 5.5 计分方式 307 | 308 | 作业满分 20 分。 309 | 310 | - 若答案错误或未能在 10 分钟内给出 $n = 1$ 时的结果,视情况给 0〜5 分 311 | - 答案正确且⾄少在 10 分钟内给出 $n=1$ 时的结果,给 5 分 312 | - 剩余 15 分将按所有有效提交程序的每秒计算量 $Ops$ 排名给出,遵循正态分布 313 | 314 | ### 5.6 作业提交方式 315 | 316 | 请将最优的源代码、Makefile/CMakeLists.txt、运行参数(包括 slurm 命令与参数)、$Ops$ 值以及实验报告打包提交到学在浙大。 截止后,我们将检查源代码的合法性(是否抄袭、使用了违禁库等)并重新编译测试。 317 | 318 | 319 | 320 | ## 6 注意事项 321 | 322 | 1. 独立完成本次实验,严禁抄袭,我们将会进行查重,一旦发现本次实验计 0 分。 323 | 2. 攻击平台、恶意浪费节点资源等干扰他人的行为,以作弊论处。 324 | 3. 只允许使用 OpenMP / Intel MPI / Intel C Compiler / GNU Compiler Colletion 等, 不允许使用 Intel MKL / BLAS / LAPACK 等第三方工具库。若最终提交的代码使用禁止的库,以作弊论处。 325 | 4. 任何问题请联系群内助教。 326 | 327 | 328 | 329 | ## 7 Bonus 330 | 331 | 本部分选做,感兴趣的同学可以尝试着完成。 332 | 333 | Bonus 部分完成即有加分(完成 Bonus 部分实验要求,且能够通过编译与测试),我们将根据完成质量提供 5-10 分的加分(与 Lab 4 权重相同)。 334 | 335 | ### 7.1 实验任务 336 | 337 | Fortran 是世界上第一个被正式采用并流传至今的高级编程语言,它因较为容易学习而被大量用于科学计算领域的代码编写。其编译器往往会随 C/C++ 的编译器一同安装,且其编译流程和 C/C++ 类似,故其能和 C/C++ 程序进行混合编译。 338 | 339 | 你需要完成的任务是,将上述实验内容使用 Fortran 重新实现,并能够和 C/C++ 代码实现混合编译(即在 Fortran 代码中调用 `hw.h` 中的 `input/output` 函数,编译生成一个由两种语言编写的二进制程序),通过 `output` 函数的测试。 340 | 341 | ### 7.2 实验要求 342 | 343 | 1. 使用 Fortran 重新编写本实验,并实现混合编译 344 | 2. 测试实现的正确性和 $Ops$ 345 | 3. 提交代码和简要的报告(可以附在 Lab 4 报告中) 346 | 347 | 你可以直接使用 Fortran 进行 Lab 4 的编写并参与排名,一起获得 Lab 4 及 Bonus 的分数。 348 | 349 | 你也可以同时提交使用 C/C++ 的实现和使用 Fortran 的实现,对比两种实现的结果,并注明使用哪个实现参与最终 Lab 4 的排名计算 Lab 4 的分数。此时建议将 Fortran 源码和 C/C++ 源码一并上交,并通过 Makefile/CMakeLists.txt 注明两种实现分别的构建与运行方式。 350 | 351 | 352 | 353 | ## 参考资料 354 | 355 | 1. [https://en.wikipedia.org/wiki/Basic_Linear_Algebra_Subprograms](https://en.wikipedia.org/wiki/Basic_Linear_Algebra_Subprograms) 356 | 2. [https://en.wikipedia.org/wiki/General_matrix_multiply](https://en.wikipedia.org/wiki/General_matrix_multiply) 357 | 3. [https://github.com/flame/how-to-optimize-gemm/wiki](https://github.com/flame/how-to-optimize-gemm/wiki) 358 | 4. [https://tvm.apache.org/docs/tutorials/optimize/opt_gemm.html](https://tvm.apache.org/docs/tutorials/optimize/opt_gemm.html) 359 | 5. Huang J, van. BLISlab: A Sandbox for Optimizing GEMM. arXiv.org. Published 2016. Accessed July 10, 2021. https://arxiv.org/abs/1609.00076 360 | 6. [计算机教育中缺失的一课 · the missing semester of your cs education (missing-semester-cn.github.io)](https://missing-semester-cn.github.io/) 361 | 7. [Introduction to Fortran (ourcodingclub.github.io)](https://ourcodingclub.github.io/tutorials/fortran-intro/) 362 | 8. [Quickstart](https://fortran-lang.org/learn/quickstart)[ tutorial - Fortran Programming Language (fortran-lang.org)](https://fortran-lang.org/learn/quickstart) 363 | -------------------------------------------------------------------------------- /docs/Lab5-DL/index.assets/CIFAR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZJUSCT/HPC101-Labs-2022/c3651b7fc444a9f064bc5273231502a73cf7aaa3/docs/Lab5-DL/index.assets/CIFAR.png -------------------------------------------------------------------------------- /docs/Lab5-DL/index.assets/Data-Parallelism.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZJUSCT/HPC101-Labs-2022/c3651b7fc444a9f064bc5273231502a73cf7aaa3/docs/Lab5-DL/index.assets/Data-Parallelism.png -------------------------------------------------------------------------------- /docs/Lab5-DL/index.assets/LeNet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZJUSCT/HPC101-Labs-2022/c3651b7fc444a9f064bc5273231502a73cf7aaa3/docs/Lab5-DL/index.assets/LeNet.jpg -------------------------------------------------------------------------------- /docs/Lab5-DL/index.assets/MNIST.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZJUSCT/HPC101-Labs-2022/c3651b7fc444a9f064bc5273231502a73cf7aaa3/docs/Lab5-DL/index.assets/MNIST.jpeg -------------------------------------------------------------------------------- /docs/Lab5-DL/index.assets/Pipeline-Parallelism.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZJUSCT/HPC101-Labs-2022/c3651b7fc444a9f064bc5273231502a73cf7aaa3/docs/Lab5-DL/index.assets/Pipeline-Parallelism.png -------------------------------------------------------------------------------- /docs/Lab5-DL/index.assets/Tensor-Parallelism.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZJUSCT/HPC101-Labs-2022/c3651b7fc444a9f064bc5273231502a73cf7aaa3/docs/Lab5-DL/index.assets/Tensor-Parallelism.png -------------------------------------------------------------------------------- /docs/Lab5-DL/index.assets/transformer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZJUSCT/HPC101-Labs-2022/c3651b7fc444a9f064bc5273231502a73cf7aaa3/docs/Lab5-DL/index.assets/transformer.png -------------------------------------------------------------------------------- /docs/Lab5-DL/index.md: -------------------------------------------------------------------------------- 1 | # 实验五:深度神经网络训练与加速 2 | 3 | ## 1 实验简介 4 | 5 | **深度学习**(Deep Learning)是[机器学习](https://zh.wikipedia.org/wiki/机器学习)的分支,是一种以[人工神经网络](https://zh.wikipedia.org/wiki/人工神经网络)为架构,对数据进行表征学习的[算法](https://zh.wikipedia.org/wiki/算法)。深度学习能够取得如此卓越的成就,除了优越的算法、充足的数据,更离不开强劲的算力。近年来,深度学习相关的基础设施逐渐成熟,从网络设计时的训练、优化,到落地的推理加速,都有非常优秀的解决方案。其中,对于算力的需求最大的部分之一是网络的训练过程,它也因此成为 HPC 领域经常研究的话题。 6 | 7 | **卷积神经网络**(Convolutional Neural Network, **CNN**)是一种[前馈神经网络](https://zh.wikipedia.org/wiki/前馈神经网络),对于大型图像处理有出色表现。 8 | 9 | **GPT**(Generative Pre-trained Transformer)是一系列自回归语言模型,目的是为了使用深度学习分类或产生人类可以理解的自然语言。GPT 系列由在旧金山的人工智能公司 OpenAI 训练与开发,模型设计基于谷歌开发的 transformer。 10 | 11 | 本次实验我们将完成 LeNet-5 和 GPT 模型的训练,并尝试使用各种优化技术加速训练过程。 12 | 13 | > 考虑到部分同学此前对于深度学习并没有很多了解,本次实验的 LeNet-5 部分主要目的为引导大家熟悉深度学习的流程,因此只需要完成即可得到分数,而 GPT 部分主要目的是引导大家尝试对于大型网络的训练进行优化,因此我们会根据模型的加速情况进行给分。 14 | 15 | ## 2 实验环境 16 | 17 | ``` bash 18 | ssh 10.15.82.243 -p 12222 19 | ``` 20 | 21 | 用户名与 Lab 4 相同。该集群也由 slurm 管理,可以使用 salloc 进行实验。集群自带 conda、pytorch、nvcc 等。集群各个节点的 home 目录是共享的,因此环境配置等可以在登录节点进行。 22 | 23 | 集群具有两个队列,每个队列有两个单 GPU 节点。debug 队列用于调试,任务时长上限为 5 分钟。normal 队列用于收敛性验证及时间的评测,任务时长上限为 20 分钟。队列选择可在 salloc 时加入 -p 参数,如希望在 debug 队列上面进行单卡训练: 24 | 25 | ``` bash 26 | srun -p debug -N 1 --pty bash 27 | ``` 28 | 29 | 任务时长上限到达时,请及时关闭任务并重新排队。如果发现节点 GPU 占用率异常,可以通过 htop 等命令观察情况,并通知助教杀死相应进程。 30 | 31 | ## 3 实验基础知识介绍 32 | 33 | ### 3.1 网络模型 34 | 35 | #### 3.1.1 CNN 卷积神经网络 36 | 37 | 卷积神经网络由一个或多个卷积层和顶端的全连通层(对应经典的神经网络)组成,同时也包括关联权重和池化层(pooling layer)。这一结构使得卷积神经网络能够利用输入数据的二维结构。与其他深度学习结构相比,卷积神经网络在图像和[语音识别](https://zh.wikipedia.org/wiki/语音识别)方面能够给出更好的结果。这一模型也可以使用[反向传播算法](https://zh.wikipedia.org/wiki/反向传播算法)进行训练。相比较其他深度、前馈神经网络,卷积神经网络需要考量的参数更少,使之成为一种颇具吸引力的深度学习结构。 38 | 39 | #### 3.1.2 LeNet-5 40 | 41 | LeNet-5是一个较简单的卷积神经网络。下图显示了其结构:输入的二维图像,先经过两次卷积层到池化层,再经过全连接层,最后输出每种分类预测得到的概率。 42 | 43 | ![](index.assets/LeNet.jpg) 44 | 45 | 有关于其更详细的结构可以在[原论文](http://yann.lecun.com/exdb/publis/pdf/lecun-01a.pdf)中找到。 46 | 47 | #### 3.1.3 GPT 48 | 49 | 在自然语言处理(Natural language processing, NLP)中,早期使用的是循环神经网络(Recurrent Neural Network, **RNN**)。RNN 与 CNN 这样的前馈网络不同,RNN 中存在反馈和隐藏单元,使它可以「记住」之前读到的内容。为了解决深层网络中梯度消失或爆炸的问题,引入了长短期记忆(Long short-term memory, **LSTM**)。而为了解决传统 RNN 只能记住前面的问题,提出了双向的 LSTM。在此基础上引入的注意力机制(attention),使得网络能注意句子中重要位置的信息,例如允许在翻译中可以改变词语的顺序。 50 | 51 | 不久后,研究者发现只靠注意力机制而无需 RNN 或 CNN,就能达到较好的效果,这就是 Transformer 模型。与 RNN 不同的是,Transformer 模型能够一次性处理所有输入数据。注意力机制可以为输入序列中的任意位置提供上下文。这种架构允许更高的并行度,并以此减少训练时间。 52 | 53 | 以下为 Transformer 的结构:包含编码器和解码器,都由多个多头自注意力机制和全连接层堆叠而成,层间和层内存在归一化操作;输入由词嵌入向量加上位置信息得出。 54 | 55 | ![](index.assets/transformer.png) 56 | 57 | Transformer 的详细结构可参考[原论文](https://arxiv.org/abs/1706.03762)。 58 | 59 | 2018 年,OpenAI 提出了生成预训练 Transformer 模型(Generative Pre-trained Transformer, **GPT**)。与先前基于监督式学习的 NLP 模型不同,GPT 在预训练生成阶段是无监督的(不需要标注),只在需要适应特定任务的**微调**(fine-tuning)时需要监督,降低了大规模 NLP 模型的门槛。GPT 的结构是 12 层仅包含解码器的 Transformer。一年后的 GPT-2 是对 GPT 的直接放大,参数量和数据集都增加了一个量级,参数量达到了 15 亿,取得了更好的效果和迁移学习能力。下一代的 GPT-3 达到了 1750 亿参数,生成的文章已经很难与人类写的区分出来。在一些领域,GPT-3 也**不再需要**专门的微调,而只需要提供例子等文本交互即可完成任务。大家可能熟悉的 GitHub Copilot 也是 GPT 的一个主要应用。GPT 系列模型的结构主要源于 Transformer 的 Encoder 部分。 60 | 61 | 本次实验要求训练一个 GPT-2/3 结构的模型,具体模型结构请参阅 [GPT-2](https://cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf) 和 [GPT-3](https://arxiv.org/abs/2005.14165) 的原论文。 62 | 63 | ### 3.2 数据集 64 | 65 | #### 3.2.1 MNIST 手写数字数据集 66 | 67 | MNIST 数据集 (Mixed National Institute of Standards and Technology database) 是美国国家标准与技术研究院收集整理的大型手写数字数据库,包含 60,000 个示例的训练集以及 10,000 个示例的测试集。 68 | 69 | How to Train a Model with MNIST dataset | by Abdullah Furkan Özbek | Medium 70 | 71 | MNIST 数据集下载:http://yann.lecun.com/exdb/mnist/index.html 72 | 73 | #### 3.2.2 Web of Science 数据集 74 | 75 | Web of Science 是一个付费的文献元数据库,通过校网可以免费使用。该数据集原先用于文本层次化分类,由于我们采用的是无监督学习的预训练,因此不关心它给出的标签,只要用目录下的 X 文本训练。元数据目录(包含摘要和关键词)也可以忽略。 76 | 77 | 数据集下载:https://data.mendeley.com/datasets/9rw3vkcfy4/6 78 | 79 | ## 4 实验步骤 80 | 81 | ### 4.1 LeNet-5 训练 82 | 83 | #### 4.1.1 数据准备 84 | 85 | 我们建议利用 `torchvision` 提供的 `torchvision.datasets` 方法导入数据,`torchvision.datasets` 所提供的接口十分方便,之后你可以用 `torch.utils.data.DataLoader` 给你的模型加载数据。 86 | 87 | 此外,我们也欢迎你自定义你的 `Dataset` 类,这样做会给你带来额外的分数。为此,你需要继承 `torch.utils.data.Dataset` 并至少需要重写其中的 `__len__()` 和 `__getitem__()` 函数,[这里](https://pytorch.org/docs/stable/data.html)有官方对 `torch.utils.data` 类的介绍,它或许可以帮到你。 88 | 89 | 幸运的是,本次实验需要用到的 `MNIST` 数据集可用 `torchvision.datasets` 导入,下面对一些你可能会用到的参数简单加以说明 90 | 91 | **注意:请在清楚参数含义后调用它们** 92 | 93 | ```Python 94 | # MNIST 95 | torchvision.datasets.MNIST(root, train=True, transform=None, target_transform=None, download=False) 96 | ``` 97 | 98 | 一些重要的参数说明: 99 | 100 | - root: 在 `MNIST`中是 `processed/training.pt` 和 `processed/test.pt` 的主目录 101 | - train: `True` 代表训练集,`False` 代表测试集 102 | - transform 和 target_transform: 分别是对图像和 label 的转换操作 103 | - download: 若为 `True` 则下载数据集并放到 `root` 所指定的目录中,否则直接尝试从 `root` 目录中读取 104 | 105 | 你可以在[这里](https://pytorch.org/vision/0.8/datasets.html)获取更加详细的说明 106 | 107 | #### 4.1.2 模型编写 108 | 109 | ##### 4.1.2.1 网络结构 110 | 111 | `PyTorch` 提供了许多种定义模型的方式,最常用的一种是将网络结构以类保存,你应当首先继承 [torch.nn.Module](https://pytorch.org/docs/stable/generated/torch.nn.Module.html#torch.nn.Module),并实现正向传播的 `forward` 函数,(为什么不用定义反向传播函数呢?因为你继承的 `nn.Module` 就是干这个事情的)。 112 | 113 | 下面为网络结构的一个 sample(但显然这样的网络并不能用于本次 Lab),本次实验中你需要自定义你的网络结构,以完成我们的分类任务: 114 | 115 | ```Python 116 | import torch.nn as nn 117 | import torch.nn.functional as F 118 | 119 | class Model(nn.Module): 120 | def __init__(self): 121 | super(Model, self).__init__() # 利用参数初始化父类 122 | self.conv1 = nn.Conv2d(1, 20, 5) 123 | self.conv2 = nn.Conv2d(20, 20, 5) 124 | 125 | def forward(self, x): 126 | x = F.relu(self.conv1(x)) 127 | return F.relu(self.conv2(x)) 128 | ``` 129 | 130 | 当然,你需要实例化你的模型,可以直接对模型打印以查看结构 131 | 132 | ```Python 133 | model = Model() 134 | print(model) 135 | ``` 136 | 137 | 网络结构编写中一个很大的难点在于每一步的 tensor shape 需要匹配,请仔细检查你的代码来确保此部分的正确性。 138 | 139 | ##### 4.1.2.2 损失函数 140 | 141 | 常见的损失函数都被定义在了 `torch.nn`中,你可以在训练过程开始前将其实例化,并在训练时调用,例如: 142 | 143 | ```Python 144 | criterion = torch.nn.CrossEntropyLoss() 145 | ``` 146 | 147 | ##### 4.1.2.3 正向传播 148 | 149 | 正向传播是指对神经网络沿着从输入层到输出层的顺序,依次计算并存储模型的中间变量(包括输出)。 150 | 正向传播的过程在 `forward`中定义,对于模型实例,可以直接利用输入输出得到模型预测的结果。 151 | 152 | ```Python 153 | y_pred = model(x) 154 | ``` 155 | 156 | ##### 4.1.2.4 反向传播 157 | 158 | 反向传播(Backpropagation,BP)是“误差反向传播”的简称,是一种与最优化方法(如梯度下降法)结合使用的,用来训练人工神经网络的常见方法。该方法对网络中所有权重计算损失函数的梯度。这个梯度会反馈给最优化方法,用来更新权值以最小化损失函数。 159 | 160 | 在计算过模型的loss之后,可以利用 `loss.backward()` 计算反向传播的梯度,梯度会被直接储存在 `requires_grad=True` 的节点中,不过此时节点的权重暂时不会更新,因此可以做到梯度的累加。 161 | 162 | ##### 4.1.2.5 优化器 163 | 164 | 常用的优化器都被定义在了 `torch.optim` 中,为了使用优化器,你需要构建一个 optimizer 对象。这个对象能够保持当前参数状态并基于计算得到的梯度进行参数更新。你需要给它一个包含了需要优化的参数(必须都是 Variable 对象)的iterable。然后,你可以设置optimizer的参数选项,比如学习率,权重衰减,例如: 165 | 166 | ```Python 167 | optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9) 168 | optimizer = optim.Adam([var1, var2], lr=0.0001) 169 | ``` 170 | 171 | 所有的optimizer都实现了step()方法,这个方法会更新所有的参数。或许你会在反向传播后用到它。 172 | 173 | ```Python 174 | optimizer.step() 175 | ``` 176 | 177 | 需要注意的是,在反向传播前,如果你不希望梯度累加,请使用下面的代码将梯度清零。 178 | 179 | ```Python 180 | optimizer.zero_grad() 181 | ``` 182 | 183 | #### 4.1.3 训练过程 184 | 185 | 前文中已经定义了网络结构、损失函数、优化器,至此,一个较为完整的训练过程如下,需要注意的是,你的训练过程要不断从 `DataLoader` 中取出数据。 186 | 187 | ```Python 188 | criterion = torch.nn.MSELoss(reduction='sum') 189 | optimizer = torch.optim.SGD(model.parameters(), lr=1e-8, momentum=0.9) 190 | for t in range(30000): 191 | # Forward pass: Compute predicted y by passing x to the model 192 | y_pred = model(x) 193 | 194 | # Compute and print loss 195 | loss = criterion(y_pred, y) 196 | 197 | # Zero gradients, perform a backward pass, and update the weights. 198 | optimizer.zero_grad() 199 | loss.backward() 200 | optimizer.step() 201 | ``` 202 | 203 | #### 4.1.4 TensorBoard 204 | 205 | TensorBoard 是常用的训练过程可视化工具。请参考 [PyTorch](https://pytorch.org/tutorials/recipes/recipes/tensorboard_with_pytorch.html) 的官方教程完成配置。 206 | 207 | #### 4.1.5 Tips 208 | 209 | - `nn.functional.ReLU` (简记为 `F.ReLU` )和 `nn.ReLU` 略有不同,区别在于前者作为一个函数调用,如 4.3.1 中所示,而后者作为一个层结构,必须添加到 `nn.Module` 容器中才能使用,两者实现的功能一样,在 `PyTorch` 中,`nn.X` 都有对应的函数版本 `F.X`。 210 | - 除了利用继承 `nn.Module` 来建立网络,不推荐但可以使用 `nn.ModuleList`, `nn.ModuleDict`,推荐使用 `nn.Sequential`直接定义模型 211 | - 你可以定义如下的 `device` 变量,以便你的模型在没有 GPU 环境下也可以测试: 212 | 213 | ```Python 214 | device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 215 | 216 | model = Model().to(device) 217 | some_data = some_data.to(device) 218 | ``` 219 | 220 | - 相比于原生的 `PyTorch`,`PyTorch Lightning` 框架对其进行了更高层次的封装,很大程度上简化了模型定义、训练以及测试的步骤,使用 `PyTorch Lightning` 作为本次实验的加分项,官网链接已附在参考资料中。如果你能够在 TensorBoard 中将中间层可视化,你能得到更多的加分。 221 | 222 | ### 4.2 GPT 训练与加速 223 | 224 | #### 4.2.1 文本数据预处理 225 | 226 | 预处理最主要的工作是分词(tokenize)。分词器将文本拆分成词,再转换成数字以供模型训练。由于句子长短不一,可能需要进行填充或截断,最后生成输入的张量。 227 | 228 | 分词的粒度,最自然的是按**单词划分**,例如英语中根据空格和标点符号划分,但生成的词表会很大,增加存储和计算复杂度;另一个极端是按**字母划分**,虽然词表会很小,但模型很难学到有意义的内容;因此现在一般使用**子词(subword)划分**,在词表中保留较短的常用词,生僻词则用常用词拼接而成。 229 | 230 | 常用的分词算法有字节对编码(Byte-Pair Encoding, **BPE**)、WordPiece、Unigram、SentencePiece 等,其中 GPT 用的是 BPE。BPE 从单个字母的词表开始,通过不断合并高频字母对,直到达到预定的词表大小。WordPiece 与 BPE 基本相同,合并策略有所区别。 231 | 232 | 具体原理介绍可参考 [https://huggingface.co/docs/transformers/tokenizer_summary](https://huggingface.co/docs/transformers/tokenizer_summary),以及 [NLP BERT GPT等模型中 tokenizer 类别说明详解](https://cloud.tencent.com/developer/article/1865689)。 233 | 234 | 可以直接使用 [huggingface 的预训练分词器](https://huggingface.co/docs/transformers/preprocessing)中提供的 GPT-2 tokenizer,如选择自己训练 vocab 可以获得 bonus。 235 | 236 | #### 4.2.2 基准代码构建与加速 237 | 238 | 请参考 3.1.3 中的模型结构描述完成基准代码的构建,并基于此进行训练加速。为了减轻工作量,此部分允许使用 huggingface transformer 等模型库,以及其他的分布式训练加速框架。但需要在报告里陈述你所采用的各项优化的原理、出发点和效果。注意本次实验中的 GPT 需要使用的是 GPT-2 的模型结构。 239 | 240 | #### 4.2.3 多卡训练 241 | 242 | 单张GPU的显存和算力是有限的,随着模型大小的增长,我们需要多张GPU一起参与训练以获得更大的显存和更高的算力。多卡训练Transformer模型时常见的并行策略有**张量并行(Tensor Parallelism)**、**流水线并行(Pipeline Parallelism)**和**数据并行(Data Parallelism)**。 243 | 244 | * 张量并行将模型层内的参数切分到不同设备进行计算,在Transformer中,注意和多层感知器(MLP)的张量在向前和向后计算时按行或列分割。 245 | ![](index.assets/Tensor-Parallelism.png) 246 | * 流水线并行将模型不同的层切分到不同设备进行计算,流水线中的每一设备接受上一节点的结果,并把自己的结果传递给下一设备。 247 | ![](index.assets/Pipeline-Parallelism.png) 248 | * 数据并行则将全局批次大小(global batch size)按照流水线分组进行分割,每个流水线组都包含模型的一个副本,数据在组内按照局部批次规模送入模型副本,最后将各组得到的梯度进行加权平均得到总的梯度。 249 | ![](index.assets/Data-Parallelism.png) 250 | 251 | 在pytorch、tensorflow等框架中都存在分布式训练的模块,为了减轻工作量,此部分也允许使用 huggingface accelerate 等模型库,以及其他的分布式训练加速框架。 252 | 253 | #### 4.2.4 模型评分规模 254 | 255 | 你需要按照下列表格中给定的模型结构参数实现模型,并按照要求的 token 数量对模型进行训练。规定 token 数量训练结束后,若模型损失低于 7,认为模型训练成功,此时训练速度越快该测试点得分越高;否则认为模型训练失败,记零分。 256 | 257 | | Model size | Hidden size | Attention-heads | Layers | Sequence length | Tokens | 258 | | :---: | :----------: | :--------------: | :----: | :-------------: | :-------------: | 259 | | 117M | 768 | 12 | 12 | 1024 | 12M | 260 | 261 | > PS. Model size 会因 vocab size 变化而波动,因此不会作为实现正确性的主要判断依据,我们会直接根据代码判断模型实现的正确性 262 | 263 | ## 5 实验任务与要求 264 | 265 | 1. 使用 `PyTorch` 实现最基本的卷积神经网络 LeNet-5,并在 MNIST 数据集上使用 GPU 进行训练,并对测试集进行测试。 266 | 2. 使用 `PyTorch` 及相关模型库实现类 GPT 网络,在 Web of Science 数据集上进行训练,并尝试对模型训练进行加速。 267 | 3. 你需要提交: 268 | 1. 全部代码 269 | 2. 实验报告,其中需要包含: 270 | 1. 简要实验过程 271 | 2. 贴上两个模型训练过程的 **GPU 占用率截图**(使用 `nvidia-smi` 查看) 272 | 3. Tensorboard **两个模型的损失曲线、LeNet-5 的准确率曲线等截图** 273 | 4. 对于 LeNet-5,你需要写明测试集上的**识别正确率** 274 | 5. 对于 GPT,你需要写明训练完成的时间,最后的收敛情况,以及使用的加速策略 275 | 4. ***LeNet-5 部分不允许直接使用各种深度学习开发工具已训练好的 CNN 网络结构与参数;GPT-2 部分不允许使用任何已经训练好的模型参数*** 276 | 5. ***本次实验依然会进行查重,如果你参考了网络上的代码请在报告中列出,并体现出你的理解,否则一经查出视为抄袭*** 277 | 278 | ## 参考资料 279 | 280 | - `PyTorch` 框架 [https://pytorch.org/](https://pytorch.org/) 281 | - `PyTorch Lightning` 框架 [https://www.pytorchlightning.ai/](https://www.pytorchlightning.ai/) 282 | - MNIST 数据集 [http://yann.lecun.com/exdb/mnist/index.html](http://yann.lecun.com/exdb/mnist/index.html) 283 | - LeNet-5 网络结构 [http://yann.lecun.com/exdb/lenet/](http://yann.lecun.com/exdb/lenet/) 284 | - GPT 网络介绍 [https://en.wikipedia.org/wiki/GPT-2](https://en.wikipedia.org/wiki/GPT-2) 285 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # 欢迎来到 HPC101 超算短学期 2 | 3 | 本网站描述了课程中所有实验,如有疑问请及时联系各位助教 4 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: HPC101-Labs-2022 2 | theme: 3 | name: material 4 | nav: 5 | - Lab1-简单集群搭建: Lab1-MiniCluster/index.md 6 | - Lab2-向量化计算: Lab2-Vectors/index.md 7 | - Lab2.5-Bonus 手写 SIMD 向量化: Lab2.5-Vectors-Bonus/index.md 8 | - Lab3-CUDA 使用基础: Lab3-Cuda/index.md 9 | - Lab4-GEMM 通用矩阵乘法: Lab4-Gemm/index.md 10 | - Lab5-深度神经网络训练与加速: Lab5-DL/index.md 11 | markdown_extensions: 12 | - pymdownx.arithmatex: 13 | generic: true 14 | - pymdownx.highlight: 15 | anchor_linenums: true 16 | - pymdownx.inlinehilite 17 | - pymdownx.snippets 18 | - pymdownx.superfences 19 | - tables 20 | extra_javascript: 21 | - javascripts/mathjax.js 22 | - https://polyfill.io/v3/polyfill.min.js?features=es6 23 | - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js 24 | --------------------------------------------------------------------------------