From 8fa5eabfee9913a873469f1914ef612cf48305f9 Mon Sep 17 00:00:00 2001 From: Tal Ben-Nun Date: Tue, 24 Mar 2020 18:28:04 +0100 Subject: [PATCH] Getting started tutorial --- README.md | 3 +- tutorials/getting_started.ipynb | 545 ++++++++++++++++++++++++++++++++ 2 files changed, 547 insertions(+), 1 deletion(-) create mode 100644 tutorials/getting_started.ipynb diff --git a/README.md b/README.md index 45778d10da..4b582ab377 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,9 @@ For more information, see our [paper](http://www.arxiv.org/abs/1902.10345). Tutorials --------- -* [Data-Centric Python Programs with NumPy](https://nbviewer.jupyter.org/github/spcl/dace/blob/master/tutorials/numpy_frontend.ipynb) +* [Getting Started](https://nbviewer.jupyter.org/github/spcl/dace/blob/master/tutorials/getting_started.ipynb) * [Explicit Dataflow in Python](https://nbviewer.jupyter.org/github/spcl/dace/blob/master/tutorials/explicit.ipynb) +* [NumPy API Reference](https://nbviewer.jupyter.org/github/spcl/dace/blob/master/tutorials/numpy_frontend.ipynb) * [SDFG API](https://nbviewer.jupyter.org/github/spcl/dace/blob/master/tutorials/sdfg_api.ipynb) * [Transformations](https://nbviewer.jupyter.org/github/spcl/dace/blob/master/tutorials/transformations.ipynb) diff --git a/tutorials/getting_started.ipynb b/tutorials/getting_started.ipynb new file mode 100644 index 0000000000..31cebfc070 --- /dev/null +++ b/tutorials/getting_started.ipynb @@ -0,0 +1,545 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Getting Started with DaCe\n", + "\n", + "DaCe is a Python library that enables optimizing code with ease, from running on a single core to a full supercomputer. With the power of data-centric transformations, it can automatically map code for CPUs, GPUs, and FPGAs.\n", + "\n", + "Let's get started with DaCe by importing it:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import dace" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A data-centric program can be generated from several general-purpose and domain-specific languages. Our main frontend, however, is Python/numpy. To define a program, we take an existing function on numpy arrays and decorate it with `@dace.program`:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "@dace.program\n", + "def getstarted(A):\n", + " return A + A" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Running our dace program, we will see several outputs and a prompt. These are the available transformations we can apply. For the first step, we opt to apply none (press Enter) and proceed with compilation and running:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[0.55928079, 0.99304442, 0.65794934],\n", + " [0.58411335, 0.77098752, 0.30517518]])" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import numpy as np\n", + "a = np.random.rand(2, 3)\n", + "a" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Applied 1 StateFusion.\n", + "0. Transformation FPGATransformSDFG in getstarted\n", + "1. Transformation FPGATransformState in BinOp_3\n", + "2. Transformation GPUTransformLocalStorage in _Add__map[__i0=0:2, __i1=0:3]\n", + "3. Transformation GPUTransformMap in _Add__map[__i0=0:2, __i1=0:3]\n", + "4. Transformation GPUTransformSDFG in getstarted\n", + "5. Transformation MapExpansion in _Add__map: ['__i0', '__i1']\n", + "6. Transformation MapTiling in _Add__map: ['__i0', '__i1']\n", + "7. Transformation NestSDFG in getstarted\n", + "8. Transformation StripMining in _Add__map: ['__i0', '__i1']\n", + "9. Transformation Vectorization in 0 -> 1 -> 2\n", + "Select the pattern to apply (0 - 9 or name$id): \n", + "You did not select a valid option. Quitting optimization ...\n", + "-- Configuring done\n", + "-- Generating done\n", + "-- Build files have been written to: /path/to/dace/tutorials/.dacecache/getstarted/build\n", + "\n", + "[ 50%] Built target getstarted\n", + "[100%] Built target dacestub_getstarted\n", + "\n" + ] + }, + { + "data": { + "text/plain": [ + "array([[1.11856158, 1.98608884, 1.31589868],\n", + " [1.1682267 , 1.54197505, 0.61035035]])" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "getstarted(a)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The results are, as expected, `2*A`.\n", + "\n", + "Now, let's inspect the intermediate representation of the data-centric program, its Stateful Dataflow Multigraph (SDFG):" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Applied 1 StateFusion.\n" + ] + }, + { + "data": { + "text/html": [ + "\n", + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "getstarted.to_sdfg(a)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can drag the handle at the bottom right to make the SDFG frame larger.\n", + "\n", + "Notice the following four elements in the graph:\n", + "\n", + "1. **State** (blue region): This is the control flow part of the application, represented as a state machine. Since there is no control-flow in the data-centric representation of `A+A`, we see only one state encompassing the computation.\n", + "2. **Arrays** (circular nodes) and **Memlets** (arrows): These nodes represent disjoint N-dimensional memory regions (similar to numpy `ndarray`s), and the edges represent data that is moved throughout the state. Hovering over a memlet will show more information about the subset being moved.\n", + "3. **Tasklets** (octagon): This node represents the computational parts of the graph. Zooming into it will show the code (addition operation in this case). Tasklets act as pure functions that can only work with the data coming into/out of its **connectors** (cyan circles on the node).\n", + "4. **Maps** (trapezoid): Anything that is enclosed between these two nodes (the map *scope*) is replicated for the number of times specified on the node (in our case, `2*3` times). This creates parametric parallelism in the graph and can be nested in each other for efficient parallelization and distribution of work.\n", + "\n", + "Unfortunately (or fortunately in some cases), this graph is specialized for a specific size of array (as given to it), and will not work on other sizes. To compile a program that works with general sizes, we'll need to use symbolic sizes." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Symbols\n", + "\n", + "DaCe includes a symbolic math engine (extending SymPy) to support symbolic expressions for sizes, ranges, accesses, and more. \n", + "\n", + "Any number of symbols can be used throughout a computation. Defining a symbol is as easy as calling:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "N = dace.symbol('N')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "which we can now use for any computation and definitions. For example, annotating the types of our function from above will yield a version that works with any size:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "@dace.program\n", + "def getstarted_sym(A: dace.float64[N, 2*N]):\n", + " return A + A" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Applied 1 StateFusion.\n" + ] + }, + { + "data": { + "text/html": [ + "\n", + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "getstarted_sym.to_sdfg()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If we compile this code, any array that can match a size of `Nx2N` will be automatically used to infer the value of `N` and invoke the function:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Applied 1 StateFusion.\n", + "0. Transformation FPGATransformSDFG in getstarted_sym\n", + "1. Transformation FPGATransformState in BinOp_3\n", + "2. Transformation GPUTransformLocalStorage in _Add__map[__i0=0:N, __i1=0:2*N]\n", + "3. Transformation GPUTransformMap in _Add__map[__i0=0:N, __i1=0:2*N]\n", + "4. Transformation GPUTransformSDFG in getstarted_sym\n", + "5. Transformation MapExpansion in _Add__map: ['__i0', '__i1']\n", + "6. Transformation MapTiling in _Add__map: ['__i0', '__i1']\n", + "7. Transformation NestSDFG in getstarted_sym\n", + "8. Transformation StripMining in _Add__map: ['__i0', '__i1']\n", + "9. Transformation Vectorization in 0 -> 1 -> 2\n", + "Select the pattern to apply (0 - 9 or name$id): \n", + "You did not select a valid option. Quitting optimization ...\n", + "-- Configuring done\n", + "-- Generating done\n", + "-- Build files have been written to: /path/to/dace/tutorials/.dacecache/getstarted_sym/build\n", + "\n", + "[ 50%] Built target getstarted_sym\n", + "[100%] Built target dacestub_getstarted_sym\n", + "\n", + "WARNING: Casting scalar argument \"N\" from Integer to \n" + ] + }, + { + "data": { + "text/plain": [ + "array([[1.7585673 , 1.4819243 , 0.81444086, ..., 0.50401245, 0.53381245,\n", + " 1.09545598],\n", + " [0.35190908, 1.72341969, 1.94377987, ..., 1.39787272, 1.82237336,\n", + " 1.12328556],\n", + " [1.48051736, 0.40447843, 0.03104017, ..., 0.92004259, 0.1404551 ,\n", + " 0.54104224],\n", + " ...,\n", + " [1.17761424, 0.80583588, 1.48981887, ..., 1.19151846, 1.76003948,\n", + " 1.43971579],\n", + " [0.79070708, 1.5751959 , 1.72468958, ..., 0.52099313, 0.50643533,\n", + " 0.58934414],\n", + " [0.78405278, 1.17276108, 1.27954205, ..., 1.69494209, 0.49935545,\n", + " 0.33314416]])" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "getstarted_sym(np.random.rand(100, 200))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Performance\n", + "\n", + "Given our symbolic SDFG, we would not like to recompile it every time. Thus, we can pre-compile the graph into an .so/.dll file:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Applied 1 StateFusion.\n", + "0. Transformation FPGATransformSDFG in getstarted_sym\n", + "1. Transformation FPGATransformState in BinOp_3\n", + "2. Transformation GPUTransformLocalStorage in _Add__map[__i0=0:N, __i1=0:2*N]\n", + "3. Transformation GPUTransformMap in _Add__map[__i0=0:N, __i1=0:2*N]\n", + "4. Transformation GPUTransformSDFG in getstarted_sym\n", + "5. Transformation MapExpansion in _Add__map: ['__i0', '__i1']\n", + "6. Transformation MapTiling in _Add__map: ['__i0', '__i1']\n", + "7. Transformation NestSDFG in getstarted_sym\n", + "8. Transformation StripMining in _Add__map: ['__i0', '__i1']\n", + "9. Transformation Vectorization in 0 -> 1 -> 2\n", + "Select the pattern to apply (0 - 9 or name$id): \n", + "You did not select a valid option. Quitting optimization ...\n", + "-- Configuring done\n", + "-- Generating done\n", + "-- Build files have been written to: /path/to/dace/tutorials/.dacecache/getstarted_sym/build\n", + "\n", + "[ 50%] Built target getstarted_sym\n", + "[100%] Built target dacestub_getstarted_sym\n", + "\n" + ] + } + ], + "source": [ + "csdfg = getstarted_sym.compile()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A compiled SDFG, however, has to be invoked like an SDFG, with keyword arguments only:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "b = csdfg(A=np.random.rand(10,20), N=np.int32(10))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now see the performance of the code on large arrays vs. numpy:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "tester = np.random.rand(2000, 4000)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "81.4 ms ± 568 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" + ] + } + ], + "source": [ + "%timeit tester + tester" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "9.92 ms ± 96.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" + ] + } + ], + "source": [ + "%timeit csdfg(A=tester, N=np.int32(2000))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Explicit Dataflow\n", + "\n", + "One can specify explicit dataflow in dace using `for i in dace.map[begin:end]:` syntax, as well as tasklets manually using `with dace.tasklet:`. Here is an example of a real-world example (Scattering Self-Energies) with an 8-dimensional parallel computation:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Applied 5 StateFusion, 1 MergeArrays, 1 InlineSDFG.\n" + ] + }, + { + "data": { + "text/html": [ + "\n", + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Declaration of symbolic variables\n", + "Nkz, NE, Nqz, Nw, N3D, NA, NB, Norb = (\n", + " dace.symbol(name)\n", + " for name in ['Nkz', 'NE', 'Nqz', 'Nw', 'N3D', 'NA', 'NB', 'Norb'])\n", + "\n", + "\n", + "@dace.program\n", + "def sse_sigma(neigh_idx: dace.int32[NA, NB],\n", + " dH: dace.complex128[NA, NB, N3D, Norb, Norb],\n", + " G: dace.complex128[Nkz, NE, NA, Norb, Norb],\n", + " D: dace.complex128[Nqz, Nw, NA, NB, N3D, N3D],\n", + " Sigma: dace.complex128[Nkz, NE, NA, Norb, Norb]):\n", + "\n", + " # Declaration of Map scope\n", + " for k, E, q, w, i, j, a, b in dace.map[0:Nkz, 0:NE, 0:Nqz, 0:Nw, 0:N3D, 0:\n", + " N3D, 0:NA, 0:NB]:\n", + " dHG = G[k - q, E - w, neigh_idx[a, b]] @ dH[a, b, i]\n", + " dHD = dH[a, b, j] * D[q, w, a, b, i, j]\n", + " Sigma[k, E, a] += dHG @ dHD\n", + " \n", + "sse_sigma.to_sdfg()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.6" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +}