├── .gitignore ├── .readthedocs.yml ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── benchmarks ├── __init__.py ├── test_compiler.py └── test_core.py ├── build.nix ├── default.nix ├── dev.nix ├── docs ├── comparison.rst ├── conf.py ├── figures │ └── moa-implementation.tikz ├── images │ ├── dnf.svg │ ├── frontend.svg │ └── shape.svg ├── implementation.rst ├── index.rst ├── introduction.rst ├── optimization.rst ├── papers │ ├── ARRAYS-2019 │ │ ├── README.md │ │ ├── acmart.cls │ │ ├── default.nix │ │ └── paper.tex │ └── JOSS-2019 │ │ └── README.md ├── presentations │ └── scipy-2019-lightning │ │ ├── README.md │ │ └── presentation.md ├── publications.rst ├── roadmap.rst └── theory.rst ├── moa ├── __init__.py ├── analysis.py ├── array.py ├── ast.py ├── backend │ ├── __init__.py │ └── python.py ├── compiler.py ├── dnf.py ├── exception.py ├── frontend │ ├── __init__.py │ ├── __main__.py │ ├── array.py │ ├── moa.py │ └── numpy.py ├── onf.py ├── shape.py ├── testing.py └── visualize.py ├── notebooks ├── 0-introduction.ipynb ├── 1-simple-example.ipynb ├── 2-simple-example-symbolic.ipynb ├── 3-lazy-arrays.ipynb ├── 4-benchmarks.ipynb ├── 5-reduce.ipynb └── 6-inner-product.ipynb ├── release.nix ├── setup.py └── tests ├── __init__.py ├── backend └── test_python.py ├── conftest.py ├── frontend ├── test_array.py └── test_moa.py ├── test_analysis.py ├── test_array.py ├── test_ast.py ├── test_compiler.py ├── test_dnf.py ├── test_integration.py ├── test_shape.py └── test_visualize.py /.gitignore: -------------------------------------------------------------------------------- 1 | # python 2 | *.pyc 3 | __pycache__ 4 | .ipynb_checkpoints 5 | 6 | # nix 7 | result 8 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | python: 4 | version: 3.7 5 | install: 6 | - method: pip 7 | path: . 8 | extra_requirements: 9 | - docs 10 | system_packages: true 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: nix 2 | script: nix-build dev.nix -A python-moa --arg benchmark true 3 | 4 | deploy: 5 | provider: script 6 | skip_cleanup: true 7 | script: >- 8 | nix-build dev.nix -A release && 9 | nix-shell -p python3Packages.twine --run "twine upload result/*" 10 | on: 11 | tags: true 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ### Added 10 | 11 | ### Changed 12 | 13 | ### Removed 14 | 15 | ## [0.5.1] - 2019-04-12 16 | 17 | ### Changed 18 | 19 | - sly is now optional dependency (moving away from lex/yacc moa syntax) 20 | 21 | ## [0.5.0] - 2019-04-11 22 | 23 | ### Added 24 | 25 | - pypi release 26 | - initial documentation 27 | - benchmarks (addition, outer product, matrix multiply, compile time) 28 | - symbolic shapes only dimension is required at compile time 29 | - support for core operations 30 | - (+-*/) 31 | - reduce(binop) 32 | - inner(binop, binop) 33 | - outer(binop) 34 | - indexing 35 | - arbitrary transpose 36 | - shape, dnf, onf compilation stages 37 | - onf stage is naive compiler (no loop reduction, loop ordering, etc.) 38 | - lex/yacc moa frontend and `LazyArray` frontend 39 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | This repository is governed by the Quansight Repository Code of Conduct. It 2 | can be found here: 3 | https://github.com/Quansight/.github/blob/master/CODE_OF_CONDUCT.md. 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Quansight-Labs 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | recursive-include tests * 3 | recursive-include benchmarks * -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/Quansight-Labs/python-moa.svg?branch=master)](https://travis-ci.org/Quansight-Labs/python-moa) 2 | [![Documentation Status](https://readthedocs.org/projects/python-moa/badge/?version=latest)](https://python-moa.readthedocs.io/en/latest/?badge=latest) 3 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/Quansight-Labs/python-moa/master) 4 | [![PyPI](https://img.shields.io/pypi/v/python-moa.svg?style=flat-square)](https://pypi.org/project/python-moa/) 5 | 6 | 7 | # Mathematics of Arrays (MOA) 8 | 9 | MOA is a mathematically rigorous approach to dealing with arrays that 10 | was developed by [Lenore 11 | Mullins](https://www.albany.edu/ceas/lenore-mullin.php). MOA is guided 12 | by the following principles. 13 | 14 | 1. Everything is an array and has a shape. Scalars. Vectors. NDArray. 15 | 16 | 2. What is the shape of the computation at each step of the calculation? 17 | 18 | Answering this guarentees no out of bounds indexing and a valid 19 | running program. 20 | 21 | 3. What are the indicies and operations required to produce a given 22 | index in the result? 23 | 24 | Once we have solved this step we have a minimal representation of the 25 | computation that has the [Church 26 | Rosser](https://en.wikipedia.org/wiki/Church%E2%80%93Rosser_theorem) 27 | property. Allowing us to truely compare algorithms, analyze 28 | algorithms, and finally map to algorithm to a low level 29 | implementation. For further questions see the 30 | [documentation](https://python-moa.readthedocs.io/en/latest/?badge=latest). The 31 | documentation provides the theory, implementation details, and a 32 | guide. 33 | 34 | Important questions that will guide development: 35 | 36 | - [X] Is a simple implementation of moa possible with only knowing the dimension? 37 | - [X] Can we represent complex operations and einsum math: requires `+red, transpose`? 38 | - [ ] What is the interface for arrays? (shape, indexing function) 39 | - [ ] How does one wrap pre-existing numerical routines? 40 | 41 | # Installation 42 | 43 | ```shell 44 | pip install python-moa 45 | ``` 46 | 47 | # Documentation 48 | 49 | Documentation is available on 50 | [python-moa.readthedocs.org](https://python-moa.readthedocs.io/en/latest/?badge=latest). The 51 | documentation provides the theory, implementation details, and a 52 | guide for development and usage of `python-moa`. 53 | 54 | # Example 55 | 56 | A few well maintained jupyter notebooks are available for 57 | experimentation with 58 | [binder](https://mybinder.org/v2/gh/Quansight-Labs/python-moa/master) 59 | 60 | ## Python Frontend AST Generation 61 | 62 | ```python 63 | from moa.frontend import LazyArray 64 | 65 | A = LazyArray(name='A', shape=(2, 3)) 66 | B = LazyArray(name='B', shape=(2, 3)) 67 | 68 | expression = ((A + B).T)[0] 69 | expression.visualize(as_text=True) 70 | ``` 71 | 72 | ``` 73 | psi(Ψ) 74 | ├── Array _a2: <1> (0) 75 | └── transpose(Ø) 76 | └── + 77 | ├── Array A: <2 3> 78 | └── Array B: <2 3> 79 | ``` 80 | 81 | ## Shape Calculation 82 | 83 | ```python 84 | expression.visualize(stage='shape', as_text=True) 85 | ``` 86 | 87 | ``` 88 | psi(Ψ): <2> 89 | ├── Array _a2: <1> (0) 90 | └── transpose(Ø): <3 2> 91 | └── +: <2 3> 92 | ├── Array A: <2 3> 93 | └── Array B: <2 3> 94 | ``` 95 | 96 | ## Reduction to DNF 97 | 98 | ```python 99 | expression.visualize(stage='dnf', as_text=True) 100 | ``` 101 | 102 | ``` 103 | +: <2> 104 | ├── psi(Ψ): <2> 105 | │ ├── Array _a6: <2> (_i3 0) 106 | │ └── Array A: <2 3> 107 | └── psi(Ψ): <2> 108 | ├── Array _a6: <2> (_i3 0) 109 | └── Array B: <2 3> 110 | ``` 111 | 112 | ## Reduction to ONF 113 | 114 | ```python 115 | expression.visualize(stage='onf', as_text=True) 116 | ``` 117 | 118 | ``` 119 | function: <2> (A B) -> _a17 120 | ├── if (not ((len(B.shape) == 2) and (len(A.shape) == 2))) 121 | │ └── error arguments have invalid dimension 122 | ├── if (not ((3 == B.shape[1]) and ((2 == B.shape[0]) and ((3 == A.shape[1]) and (2 == A.shape[0]))))) 123 | │ └── error arguments have invalid shape 124 | ├── initialize: <2> _a17 125 | └── loop: <2> _i3 126 | └── assign: <2> 127 | ├── psi(Ψ): <2> 128 | │ ├── Array _a18: <1> (_i3) 129 | │ └── Array _a17: <2> 130 | └── +: <2> 131 | ├── psi(Ψ): <2> 132 | │ ├── Array _a6: <2> (_i3 0) 133 | │ └── Array A: <2 3> 134 | └── psi(Ψ): <2> 135 | ├── Array _a6: <2> (_i3 0) 136 | └── Array B: <2 3> 137 | ``` 138 | 139 | ## Generate Python Source 140 | 141 | ```python 142 | print(expression.compile(backend='python', use_numba=True)) 143 | ``` 144 | 145 | ```python 146 | @numba.jit 147 | def f(A, B): 148 | 149 | if (not ((len(B.shape) == 2) and (len(A.shape) == 2))): 150 | 151 | raise Exception('arguments have invalid dimension') 152 | 153 | if (not ((3 == B.shape[1]) and ((2 == B.shape[0]) and ((3 == A.shape[1]) and (2 == A.shape[0]))))): 154 | 155 | raise Exception('arguments have invalid shape') 156 | 157 | _a17 = numpy.zeros((2,)) 158 | 159 | for _i3 in range(0, 2): 160 | 161 | _a17[(_i3,)] = (A[(_i3, 0)] + B[(_i3, 0)]) 162 | return _a17 163 | ``` 164 | 165 | # Development 166 | 167 | Download [nix](https://nixos.org/nix/download.html). No other 168 | dependencies and all builds will be identical on Linux and OSX. 169 | 170 | ## Demoing 171 | 172 | `jupyter` environment 173 | 174 | ``` 175 | nix-shell dev.nix -A jupyter-shell 176 | ``` 177 | 178 | `ipython` environment 179 | 180 | ``` 181 | nix-shell dev.nix -A ipython-shell 182 | ``` 183 | 184 | ## Testing 185 | 186 | ``` 187 | nix-build dev.nix -A python-moa 188 | ``` 189 | 190 | To include benchmarks (numba, numpy, pytorch, tensorflow) 191 | 192 | ``` 193 | nix-build dev.nix -A python-moa --arg benchmark true 194 | ``` 195 | 196 | ## Documentation 197 | 198 | ``` 199 | nix-build dev.nix -A docs 200 | firefox result/index.html 201 | ``` 202 | 203 | ## Docker 204 | 205 | ``` 206 | nix-build moa.nix -A docker 207 | docker load < result 208 | ``` 209 | 210 | # Development Philosophy 211 | 212 | This is a proof of concept which should be guided by assumptions and 213 | goals. 214 | 215 | 1. Assumes that dimension is each operation is known. This condition 216 | with not much work can be relaxed to knowing an upper bound. 217 | 218 | 2. The MOA compiler is designed to be modular with clear separations: 219 | parsing, shape calculation, dnf reduction, onf reduction, and code 220 | generation. 221 | 222 | 3. All code is written with the idea that the logic can ported to any 223 | low level language (C for example). This means no object oriented 224 | design and using simple data structures. Dictionaries should be the 225 | highest level data structure used. 226 | 227 | 4. Performance is not a huge concern instead readability should be 228 | preferred. The goal of this code is to serve as documentation for 229 | beginners in MOA. Remember that tests are often great forms of 230 | documentation as well. 231 | 232 | 5. Runtime dependencies should be avoided. Testing (pytest, hypothesis) 233 | and Visualization (graphviz) are examples of suitable exceptions. 234 | 235 | # Contributing 236 | 237 | Contributions are welcome! For bug reports or requests please submit an issue. 238 | 239 | # Authors 240 | 241 | The original author is [Christopher 242 | Ostrouchov](https://github.com/costrouc). The funding that made this 243 | project possible came from [Quansight 244 | LLC](https://www.quansight.com/). 245 | -------------------------------------------------------------------------------- /benchmarks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quansight-Labs/python-moa/1f02519425ab0215896a5fb9be00631c20d34895/benchmarks/__init__.py -------------------------------------------------------------------------------- /benchmarks/test_compiler.py: -------------------------------------------------------------------------------- 1 | from moa.frontend import LazyArray 2 | 3 | 4 | def test_moa_compile_simple(benchmark): 5 | A = LazyArray(name='A', shape=('n', 'm')) 6 | B = LazyArray(name='B', shape=('k', 'l')) 7 | 8 | expression = A + B 9 | 10 | def _test(): 11 | expression.compile(backend='python', use_numba=True) 12 | 13 | benchmark(_test) 14 | 15 | 16 | def test_moa_compile_complex(benchmark): 17 | A = LazyArray(name='A', shape=('n', 'm')) 18 | B = LazyArray(name='B', shape=('k', 'l')) 19 | C = LazyArray(name='C', shape=(10, 5)) 20 | 21 | expression = (A.inner('+', '*', B)).T[0] + C.reduce('+') 22 | 23 | def _test(): 24 | expression.compile(backend='python', use_numba=True) 25 | 26 | benchmark(_test) 27 | -------------------------------------------------------------------------------- /benchmarks/test_core.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy 3 | import numba 4 | import torch 5 | import tensorflow 6 | 7 | from moa.frontend import LazyArray 8 | 9 | 10 | @pytest.mark.benchmark(group="addition", warmup=True) 11 | def test_moa_numba_addition(benchmark): 12 | n = 1000 13 | m = 1000 14 | 15 | expression = LazyArray(name='A', shape=('n', 'm')) + LazyArray(name='B', shape=('n', 'm')) 16 | 17 | local_dict = {} 18 | exec(expression.compile(backend='python', use_numba=True), globals(), local_dict) 19 | 20 | A = numpy.random.random((n, m)) 21 | B = numpy.random.random((n, m)) 22 | 23 | benchmark(local_dict['f'], A, B) 24 | 25 | 26 | @pytest.mark.benchmark(group="addition") 27 | def test_numpy_addition(benchmark): 28 | n = 1000 29 | m = 1000 30 | 31 | A = numpy.random.random((n, m)) 32 | B = numpy.random.random((n, m)) 33 | 34 | def _test(): 35 | A + B 36 | 37 | benchmark(_test) 38 | 39 | 40 | @pytest.mark.benchmark(group="addition") 41 | def test_pytorch_addition(benchmark): 42 | n = 1000 43 | m = 1000 44 | 45 | A = torch.rand(n, m) 46 | B = torch.rand(n, m) 47 | 48 | def _test(): 49 | torch.add(A, B) 50 | 51 | benchmark(_test) 52 | 53 | 54 | @pytest.mark.benchmark(group="addition") 55 | def test_tensorflow_addition(benchmark): 56 | n = 1000 57 | m = 1000 58 | 59 | A = tensorflow.random.uniform((n, m)) 60 | B = tensorflow.random.uniform((n, m)) 61 | 62 | session = tensorflow.Session() 63 | session.run(tensorflow.initialize_all_variables()) 64 | 65 | result = tensorflow.math.add(A, B) 66 | 67 | def _test(): 68 | session.run(result) 69 | 70 | benchmark(_test) 71 | 72 | 73 | @pytest.mark.benchmark(group="addition_index", warmup=True) 74 | def test_moa_numba_addition_index(benchmark): 75 | n = 1000 76 | m = 1000 77 | 78 | expression = (LazyArray(name='A', shape=('n', 'm')) + LazyArray(name='B', shape=('n', 'm')))[0] 79 | 80 | local_dict = {} 81 | exec(expression.compile(backend='python', use_numba=True), globals(), local_dict) 82 | 83 | A = numpy.random.random((n, m)) 84 | B = numpy.random.random((n, m)) 85 | 86 | benchmark(local_dict['f'], A, B) 87 | 88 | 89 | @pytest.mark.benchmark(group="addition_index") 90 | def test_numpy_addition_index(benchmark): 91 | n = 1000 92 | m = 1000 93 | 94 | A = numpy.random.random((n, m)) 95 | B = numpy.random.random((n, m)) 96 | 97 | def _test(): 98 | A[0] + B[0] 99 | 100 | benchmark(_test) 101 | 102 | 103 | @pytest.mark.benchmark(group="addition_index") 104 | def test_pytorch_addition_index(benchmark): 105 | n = 1000 106 | m = 1000 107 | 108 | A = torch.rand(n, m) 109 | B = torch.rand(n, m) 110 | 111 | def _test(): 112 | torch.add(A[0], B[0]) 113 | 114 | benchmark(_test) 115 | 116 | 117 | @pytest.mark.benchmark(group="addition_index") 118 | def test_tensorflow_addition_index(benchmark): 119 | n = 1000 120 | m = 1000 121 | 122 | A = tensorflow.random.uniform((n, m)) 123 | B = tensorflow.random.uniform((n, m)) 124 | index = tensorflow.constant(0) 125 | 126 | session = tensorflow.Session() 127 | session.run(tensorflow.initialize_all_variables()) 128 | 129 | result = tensorflow.gather(tensorflow.math.add(A, B), index) 130 | 131 | def _test(): 132 | session.run(result) 133 | 134 | benchmark(_test) 135 | 136 | 137 | @pytest.mark.benchmark(group="double_addition", warmup=True) 138 | def test_moa_numba_double_addition(benchmark): 139 | n = 1000 140 | m = 1000 141 | 142 | expression = LazyArray(name='A', shape=('n', 'm')) + LazyArray(name='B', shape=('n', 'm')) + LazyArray(name='C', shape=('n', 'm')) 143 | 144 | local_dict = {} 145 | exec(expression.compile(backend='python', use_numba=True), globals(), local_dict) 146 | 147 | A = numpy.random.random((n, m)) 148 | B = numpy.random.random((n, m)) 149 | C = numpy.random.random((n, m)) 150 | 151 | benchmark(local_dict['f'], A=A, B=B, C=C) 152 | 153 | 154 | @pytest.mark.benchmark(group="double_addition") 155 | def test_numpy_double_addition(benchmark): 156 | n = 1000 157 | m = 1000 158 | 159 | A = numpy.random.random((n, m)) 160 | B = numpy.random.random((n, m)) 161 | C = numpy.random.random((n, m)) 162 | 163 | def _test(): 164 | A + B + C 165 | 166 | benchmark(_test) 167 | 168 | 169 | @pytest.mark.benchmark(group="double_addition") 170 | def test_pytorch_double_addition(benchmark): 171 | n = 1000 172 | m = 1000 173 | 174 | A = torch.rand(n, m) 175 | B = torch.rand(n, m) 176 | C = torch.rand(n, m) 177 | 178 | def _test(): 179 | torch.add(torch.add(A, B), C) 180 | 181 | benchmark(_test) 182 | 183 | 184 | @pytest.mark.benchmark(group="double_addition") 185 | def test_tensorflow_double_addition(benchmark): 186 | n = 1000 187 | m = 1000 188 | 189 | A = tensorflow.random.uniform((n, m)) 190 | B = tensorflow.random.uniform((n, m)) 191 | C = tensorflow.random.uniform((n, m)) 192 | 193 | session = tensorflow.Session() 194 | session.run(tensorflow.initialize_all_variables()) 195 | 196 | result = tensorflow.math.add(tensorflow.math.add(A, B), C) 197 | 198 | def _test(): 199 | session.run(result) 200 | 201 | benchmark(_test) 202 | 203 | 204 | @pytest.mark.benchmark(group="outer_product", warmup=True) 205 | def test_moa_numba_outer_product(benchmark): 206 | n = 100 207 | m = 100 208 | 209 | expression = LazyArray(name='A', shape=('n', 'm')).outer('*', LazyArray(name='B', shape=('n', 'm'))) 210 | 211 | local_dict = {} 212 | exec(expression.compile(backend='python', use_numba=True), globals(), local_dict) 213 | 214 | A = numpy.random.random((n, m)) 215 | B = numpy.random.random((n, m)) 216 | 217 | benchmark(local_dict['f'], A, B) 218 | 219 | 220 | @pytest.mark.benchmark(group="outer_product") 221 | def test_numpy_outer_product(benchmark): 222 | n = 100 223 | m = 100 224 | 225 | A = numpy.random.random((n, m)) 226 | B = numpy.random.random((n, m)) 227 | 228 | def _test(): 229 | numpy.outer(A, B) 230 | 231 | benchmark(_test) 232 | 233 | 234 | @pytest.mark.benchmark(group="reduce", warmup=True) 235 | def test_moa_numba_reduce(benchmark): 236 | n = 1000 237 | m = 1000 238 | 239 | expression = LazyArray(name='A', shape=('n', 'm')).reduce('+') 240 | 241 | local_dict = {} 242 | exec(expression.compile(backend='python', use_numba=True), globals(), local_dict) 243 | 244 | A = numpy.random.random((n, m)) 245 | 246 | benchmark(local_dict['f'], A) 247 | 248 | 249 | @pytest.mark.benchmark(group="reduce") 250 | def test_numpy_reduce(benchmark): 251 | n = 1000 252 | m = 1000 253 | 254 | A = numpy.random.random((n, m)) 255 | 256 | def _test(): 257 | A.sum() 258 | 259 | benchmark(_test) 260 | 261 | 262 | @pytest.mark.benchmark(group="reduce") 263 | def test_pytorch_reduce(benchmark): 264 | n = 1000 265 | m = 1000 266 | 267 | A = torch.rand(n, m) 268 | 269 | def _test(): 270 | torch.sum(A) 271 | 272 | benchmark(_test) 273 | 274 | 275 | @pytest.mark.benchmark(group="reduce") 276 | def test_tensorflow_reduce(benchmark): 277 | n = 1000 278 | m = 1000 279 | 280 | A = tensorflow.random.uniform((n, m)) 281 | 282 | session = tensorflow.Session() 283 | session.run(tensorflow.initialize_all_variables()) 284 | 285 | result = tensorflow.math.reduce_sum(A) 286 | 287 | def _test(): 288 | session.run(result) 289 | 290 | benchmark(_test) 291 | 292 | 293 | @pytest.mark.benchmark(group="inner_product", warmup=True) 294 | def test_moa_numba_inner_product(benchmark): 295 | n = 1000 296 | m = 1000 297 | 298 | _A = LazyArray(name='A', shape=('n', 'm')) 299 | _B = LazyArray(name='B', shape=('m', 'k')) 300 | expression = _A.inner('+', '*', _B) 301 | 302 | local_dict = {} 303 | exec(expression.compile(backend='python', use_numba=True), globals(), local_dict) 304 | 305 | A = numpy.random.random((n, m)) 306 | B = numpy.random.random((n, m)) 307 | 308 | benchmark(local_dict['f'], A, B) 309 | 310 | 311 | @pytest.mark.benchmark(group="inner_product") 312 | def test_numpy_inner_product(benchmark): 313 | n = 1000 314 | m = 1000 315 | 316 | A = numpy.random.random((n, m)) 317 | B = numpy.random.random((n, m)) 318 | 319 | def _test(): 320 | A.dot(B) 321 | 322 | benchmark(_test) 323 | 324 | 325 | @pytest.mark.benchmark(group="inner_product") 326 | def test_pytorch_inner_product(benchmark): 327 | n = 1000 328 | m = 1000 329 | 330 | A = torch.rand(n, m) 331 | B = torch.rand(n, m) 332 | 333 | def _test(): 334 | torch.mm(A, B) 335 | 336 | benchmark(_test) 337 | 338 | 339 | @pytest.mark.benchmark(group="inner_product") 340 | def test_tensorflow_inner_product(benchmark): 341 | n = 1000 342 | m = 1000 343 | 344 | A = tensorflow.random.uniform((n, m)) 345 | B = tensorflow.random.uniform((n, m)) 346 | 347 | session = tensorflow.Session() 348 | session.run(tensorflow.initialize_all_variables()) 349 | 350 | result = tensorflow.linalg.matmul(A, B) 351 | 352 | def _test(): 353 | session.run(result) 354 | 355 | benchmark(_test) 356 | -------------------------------------------------------------------------------- /build.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {}, pythonPackages ? pkgs.python3Packages, benchmark ? false }: 2 | 3 | rec { 4 | package = pythonPackages.buildPythonPackage rec { 5 | name = "python-moa"; 6 | 7 | src = builtins.filterSource 8 | (path: _: !builtins.elem (builtins.baseNameOf path) [".git" "result" "docs"]) 9 | ./.; 10 | 11 | propagatedBuildInputs = with pythonPackages; [ 12 | sly 13 | astunparse 14 | ]; 15 | 16 | checkInputs = with pythonPackages; [ 17 | pytest 18 | pytestcov 19 | graphviz 20 | ] ++ (if benchmark then [ 21 | pytest-benchmark 22 | numpy 23 | numba 24 | pytorch 25 | tensorflow 26 | ] else [ ]); 27 | 28 | checkPhase = '' 29 | pytest tests ${if benchmark then "benchmarks" else ""} --cov=moa 30 | ''; 31 | }; 32 | 33 | sdist = pkgs.stdenv.mkDerivation { 34 | name = "python-moa-sdist"; 35 | 36 | buildInputs = [ package ]; 37 | 38 | src = builtins.filterSource 39 | (path: _: !builtins.elem (builtins.baseNameOf path) [".git" "result"]) 40 | ./.; 41 | 42 | buildPhase = '' 43 | python setup.py sdist 44 | ''; 45 | 46 | installPhase = '' 47 | mkdir -p $out 48 | cp dist/* $out 49 | ''; 50 | }; 51 | 52 | docs = pkgs.stdenv.mkDerivation { 53 | name = "python-moa-docs"; 54 | 55 | src = builtins.filterSource 56 | (path: _: !builtins.elem (builtins.baseNameOf path) [".git" "result"]) 57 | ./.; 58 | 59 | postPatch = '' 60 | # readthedocs makes the default GhostScript 61 | substituteInPlace docs/conf.py \ 62 | --replace "'GhostScript'" "'pdf2svg'" 63 | ''; 64 | 65 | buildInputs = with pythonPackages; [ 66 | package 67 | sphinx 68 | # sphinxcontrib-tikz 69 | ]; 70 | 71 | buildPhase = '' 72 | cd docs; 73 | sphinx-apidoc -f -o source/ ../moa 74 | sphinx-build -b doctest . _build/doctest 75 | sphinx-build -b html . _build/html 76 | ''; 77 | 78 | installPhase = '' 79 | mkdir $out 80 | cp -r _build/html/* $out 81 | ''; 82 | }; 83 | 84 | docker = pkgs.dockerTools.buildLayeredImage { 85 | name = "python-moa"; 86 | tag = "latest"; 87 | contents = [ 88 | (pythonPackages.python.withPackages (ps: with ps; [ package ipython ])) 89 | ]; 90 | config.Cmd = [ "ipython" ]; 91 | maxLayers = 120; 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | let dev = import ./dev.nix { benchmark = false; }; 2 | in dev.binder 3 | -------------------------------------------------------------------------------- /dev.nix: -------------------------------------------------------------------------------- 1 | { benchmark ? false }: 2 | 3 | let 4 | # pin version 5 | pkgs = import (builtins.fetchTarball { 6 | url = "https://github.com/costrouc/nixpkgs/archive/14775a074cfacc59482d1adf0446801d38c08216.tar.gz"; 7 | sha256 = "152dflinv7a0nk267nc1i3ldvrx5fwxm7cf3igxc0qnd92n82phf"; 8 | }) { }; 9 | 10 | pythonPackages = pkgs.python3Packages; 11 | 12 | buildInputs = with pythonPackages; [ astunparse ]; 13 | docsInputs = with pythonPackages; [ sphinx sphinxcontrib-tikz ]; 14 | testInputs = with pythonPackages; [ pytest pytestcov graphviz sly ]; 15 | benchmarkInputs = with pythonPackages; [ pytest-benchmark numpy numba pytorch tensorflow ]; 16 | in 17 | rec { 18 | python-moa = pythonPackages.buildPythonPackage { 19 | name = "python-moa"; 20 | 21 | src = builtins.filterSource 22 | (path: _: !builtins.elem (builtins.baseNameOf path) [".git" "result" "docs"]) 23 | ./.; 24 | 25 | propagatedBuildInputs = buildInputs; 26 | checkInputs = if benchmark then testInputs ++ benchmarkInputs else testInputs; 27 | 28 | checkPhase = '' 29 | pytest tests ${if benchmark then "benchmarks" else ""} --cov=moa 30 | ''; 31 | }; 32 | 33 | release = pkgs.stdenv.mkDerivation { 34 | name = "python-moa-sdist"; 35 | 36 | buildInputs = [ python-moa ]; 37 | 38 | src = builtins.filterSource 39 | (path: _: !builtins.elem (builtins.baseNameOf path) [".git" "result"]) 40 | ./.; 41 | 42 | buildPhase = '' 43 | python setup.py sdist 44 | ''; 45 | 46 | installPhase = '' 47 | mkdir -p $out 48 | cp dist/* $out 49 | ''; 50 | }; 51 | 52 | docs = pkgs.stdenv.mkDerivation { 53 | name = "python-moa-docs"; 54 | 55 | src = builtins.filterSource 56 | (path: _: !builtins.elem (builtins.baseNameOf path) [".git" "result"]) 57 | ./.; 58 | 59 | postPatch = '' 60 | # readthedocs makes the default GhostScript 61 | substituteInPlace docs/conf.py \ 62 | --replace "'GhostScript'" "'pdf2svg'" 63 | ''; 64 | 65 | buildInputs = [ python-moa ] ++ docsInputs; 66 | 67 | buildPhase = '' 68 | cd docs; 69 | sphinx-apidoc -f -o source/ ../moa 70 | sphinx-build -b doctest . _build/doctest 71 | sphinx-build -b html . _build/html 72 | ''; 73 | 74 | installPhase = '' 75 | mkdir $out 76 | cp -r _build/html/* $out 77 | ''; 78 | }; 79 | 80 | # docker load < result 81 | docker = pkgs.dockerTools.buildLayeredImage { 82 | name = "python-moa"; 83 | tag = "latest"; 84 | contents = [ 85 | (pythonPackages.python.withPackages (ps: with ps; [ python-moa ipython ])) 86 | ]; 87 | config.Cmd = [ "ipython" ]; 88 | maxLayers = 120; 89 | }; 90 | 91 | ipython-shell = pkgs.mkShell { 92 | buildInputs = with pythonPackages; [ python-moa ipython ]; 93 | 94 | shellHook = '' 95 | cd notebooks; ipython 96 | ''; 97 | }; 98 | 99 | jupyter-shell = pkgs.mkShell { 100 | buildInputs = with pythonPackages; [ python-moa jupyterlab graphviz numpy numba]; 101 | 102 | shellHook = '' 103 | cd notebooks; jupyter lab 104 | ''; 105 | }; 106 | 107 | binder = pkgs.mkShell { 108 | buildInputs = with pythonPackages; [ python-moa jupyterlab graphviz numpy numba]; 109 | }; 110 | 111 | } 112 | -------------------------------------------------------------------------------- /docs/comparison.rst: -------------------------------------------------------------------------------- 1 | Comparisons 2 | =========== 3 | 4 | MOA is not unique in its attempt to be a high level tensor 5 | compiler. Thus we find it important to contrast it with others 6 | approaches and show the landscape of available compilers. 7 | 8 | - `MLIR (Google) `_ 9 | - `PlaidML (Intel) `_ 10 | - `TensorComprehensions (Facebook) `_ 11 | - `ngraph (Intel) `_ 12 | - `taco `_ 13 | - `tvm `_ 14 | - `xla (Google) `_ 15 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | project = 'python-moa' 2 | copyright = '2019, Quansight' 3 | author = 'Quansight' 4 | version = '0.0.1' 5 | release = '0.0.1' 6 | extensions = [ 7 | 'sphinx.ext.autodoc', 8 | 'sphinx.ext.doctest', 9 | 'sphinx.ext.mathjax', 10 | 'sphinxcontrib.tikz', 11 | ] 12 | templates_path = ['_templates'] 13 | source_suffix = '.rst' 14 | master_doc = 'index' 15 | language = None 16 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 17 | pygments_style = 'sphinx' 18 | html_theme = 'alabaster' 19 | 20 | tikz_proc_suite = 'GhostScript' 21 | tikz_transparent = True 22 | 23 | doctest_global_setup = ''' 24 | from moa import ast 25 | ''' 26 | 27 | mathjax_config = { 28 | 'TeX': { 29 | 'Macros': { 30 | # Math notation 31 | "Z": "\\mathbb{Z}", # set of integers 32 | # MoA notations 33 | "minus": "{}^{\\boldsymbol{\\mbox{-}}\\!}", # scalar negation operator 34 | "rop": ["\\,\\mathrm{#1}^*\\,", 1], # relational operation 35 | "op": ["\\,\\mathrm{#1}\\,}", 1], # binary operations 36 | "uop": ["\\mathrm{#1}\\,", 1], # unary operation 37 | "hop": ["{{}_{#1}\\!\\Omega_{#2}}\\,", 2], # higher order operation 38 | "id": ["\\mathrm{id}(\\op{#1})", 1], # identity of operations 39 | "dims": "\\delta\\,", # array dimension operator 40 | "shape": "\\rho\\,", # array shape operator 41 | "size": "\\tau\\,", # array size operator 42 | "reshape": "\\,\\widehat{\\rho}\\,", # reshape operator 43 | "drop": "\\,\\nabla\\,", # drop operator 44 | "take": "\\,\\Delta\\,", # take operator 45 | "product": "\\pi\\,", # product operator 46 | # DeclareMathOperator{\\rav}{rav} 47 | "ravel": "\\rav\\,", # ravel operator 48 | "range": "\\iota\\,", # range operator 49 | "transpose": "\\bigcirc\\!\\!\\!\\!\\!\\backslash\\;", # transpose operator, need a better symbol 50 | "vc": ["<#1>", 1], # vector with one component 51 | "vcc": ["<#1\\;#2>", 2], # vector with two components 52 | "vccc": ["<#1\\;#2\\;#3>", 3], # vector with three components 53 | "vcccc": ["<#1\\;#2\\;#3\\;#4>", 4], # vector with four components 54 | "ac": ["[\\;#1\\;]", 1], # array with one components 55 | "acc": ["[\\;#1\\;#2\\;>", 2], # array with two components 56 | "accc": ["[\\;#1\\;#2\\;#3\\;]", 3], # array with three components 57 | "acccc": ["[\\;#1\\;#2\\;#3\\;#4\\;]", 4], # array with four components 58 | "avcc": ["[\\;<#1>\\;<#2>\\;]", 2], # three dimensionar array with two components 59 | "aacc": ["[\\;[\\;#1\\;]\\;[\\;#2\\;]\\;]", 2], # four dimensionar array with two components 60 | "aaccIcc": ["[\\;[\\;#1\\;#2\\;]\\;[\\;#3\\;#4\\;]\\;]", 4], # four dimensionar array with two components 61 | "outerprod": ["\\,\\bullet_{#1}\\,", 1], # outer product opetation 62 | "innerprod": ["\\,{}_{#1}\\!\\!\\bullet_{#2}\\,", 2], # inner product opetation 63 | # DeclareMathOperator{\\red}{red} 64 | "reduce": ["{}_{#1}\\!\\red\\,", 1], # reduce operator 65 | "getitem": ["{#2}\\,\\psi\\,{#1}", 2], # psi operator 66 | "scan": ["{}_{\\op{#1}\\!}\\mathrm{scan}\\,", 1], 67 | "kron": "\\bigcirc\\,\\!\\!\\!\\!\\!\\!\\times\\;", 68 | "cat": "+\\!\\!\\!+", 69 | "gu": "\\mathrm{gu}\\,", 70 | "gd": "\\mathrm{gd}\\,", 71 | "compress": "\\,\\notslash\\,", 72 | "expand": "\\,\\notbackslash\\,", 73 | "reverse": "\\phi\\,", 74 | "rotate": ["{#1}\\theta\\,", 1] 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /docs/figures/moa-implementation.tikz: -------------------------------------------------------------------------------- 1 | \begin{tikzpicture}[ 2 | basic box/.style = { 3 | shape = rectangle, 4 | align = center, 5 | draw = #1!60, 6 | fill = #1!5, 7 | rounded corners}, 8 | squarednode/.style={rectangle, draw=blue!40, fill=blue!5, very thick, minimum size=5mm}, 9 | ] 10 | %Nodes 11 | \node[squarednode] (shape-analysis) {(1) Shape Analysis}; 12 | \node[squarednode] (dnf-reduction) [below=of shape-analysis] {(2) DNF Reduction}; 13 | \node[squarednode] (onf-reduction) [below=of dnf-reduction] {(3) ONF Reduction}; 14 | 15 | \begin{scope}[on background layer] 16 | \node[fit = (shape-analysis)(dnf-reduction)(onf-reduction), basic box = black] (moa-compiler) {MOA Compiler}; 17 | \end{scope} 18 | 19 | \node[squarednode] (frontend) [above=of moa-compiler] {Frontend (Internal AST)}; 20 | \node[squarednode] (frontend-numpy) [above left=of frontend] {Numpy}; 21 | \node[squarednode] (frontend-moa) [above=of frontend] {MOA}; 22 | \node[squarednode] (frontend-tensorflow) [above right=of frontend] {Tensorflow}; 23 | 24 | \node[squarednode] (backend) [below=of moa-compiler] {Backend (Internal Reduced AST)}; 25 | \node[squarednode] (backend-c) [below left=of backend] {C}; 26 | \node[squarednode] (backend-numpy) [below=of backend] {numpy}; 27 | 28 | %Lines 29 | \draw[->] (shape-analysis.south) -- (dnf-reduction.north); 30 | \draw[->] (dnf-reduction.south) -- (onf-reduction.north); 31 | 32 | \draw[->] (moa-compiler.south) -- (backend.north); 33 | \draw[->] (backend) -- (backend-c); 34 | \draw[->] (backend) -- (backend-numpy); 35 | 36 | \draw[->] (frontend.south) -- (moa-compiler.north); 37 | \draw[->] (frontend-numpy) -- (frontend); 38 | \draw[->] (frontend-moa) -- (frontend); 39 | \draw[->] (frontend-tensorflow) -- (frontend); 40 | \end{tikzpicture} 41 | -------------------------------------------------------------------------------- /docs/images/dnf.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | %3 11 | 12 | 13 | 14 | 0 15 | 16 | 17 | condition 18 | 19 | <> 20 | 21 | ((0 <= n) and ((m == l) and (n == k))) 22 | 23 | 24 | 25 | 1 26 | 27 | 28 | reduce (+) 29 | 30 | <> 31 | 32 | _i10 33 | 34 | 35 | 36 | 0->1 37 | 38 | 39 | 40 | 41 | 42 | 2 43 | 44 | 45 | + 46 | 47 | <> 48 | 49 | 50 | 51 | 1->2 52 | 53 | 54 | 55 | 56 | 57 | 3 58 | 59 | 60 | psi(Ψ) 61 | 62 | <> 63 | 64 | 65 | 66 | 2->3 67 | 68 | 69 | 70 | 71 | 72 | 6 73 | 74 | 75 | psi(Ψ) 76 | 77 | <> 78 | 79 | 80 | 81 | 2->6 82 | 83 | 84 | 85 | 86 | 87 | 4 88 | 89 | 90 | Array _a12 91 | 92 | <2> 93 | 94 | (0 _i10) 95 | 96 | 97 | 98 | 3->4 99 | 100 | 101 | 102 | 103 | 104 | 5 105 | 106 | 107 | Array A 108 | 109 | <n m> 110 | 111 | 112 | 113 | 3->5 114 | 115 | 116 | 117 | 118 | 119 | 7 120 | 121 | 122 | Array _a12 123 | 124 | <2> 125 | 126 | (0 _i10) 127 | 128 | 129 | 130 | 6->7 131 | 132 | 133 | 134 | 135 | 136 | 8 137 | 138 | 139 | Array B 140 | 141 | <k l> 142 | 143 | 144 | 145 | 6->8 146 | 147 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /docs/images/frontend.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | %3 11 | 12 | 13 | 14 | 0 15 | 16 | psi(Ψ) 17 | 18 | 19 | 20 | 1 21 | 22 | 23 | Array _a6 24 | 25 | <1> 26 | 27 | (0) 28 | 29 | 30 | 31 | 0->1 32 | 33 | 34 | 35 | 36 | 37 | 2 38 | 39 | reduce (+) 40 | 41 | 42 | 43 | 0->2 44 | 45 | 46 | 47 | 48 | 49 | 3 50 | 51 | transpose(Ø) 52 | 53 | 54 | 55 | 2->3 56 | 57 | 58 | 59 | 60 | 61 | 4 62 | 63 | + 64 | 65 | 66 | 67 | 3->4 68 | 69 | 70 | 71 | 72 | 73 | 5 74 | 75 | 76 | Array A 77 | 78 | <n m> 79 | 80 | 81 | 82 | 4->5 83 | 84 | 85 | 86 | 87 | 88 | 6 89 | 90 | 91 | Array B 92 | 93 | <k l> 94 | 95 | 96 | 97 | 4->6 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /docs/images/shape.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | %3 11 | 12 | 13 | 14 | 0 15 | 16 | 17 | condition 18 | 19 | <> 20 | 21 | ((0 <= n) and ((m == l) and (n == k))) 22 | 23 | 24 | 25 | 1 26 | 27 | 28 | psi(Ψ) 29 | 30 | <> 31 | 32 | 33 | 34 | 0->1 35 | 36 | 37 | 38 | 39 | 40 | 2 41 | 42 | 43 | Array _a6 44 | 45 | <1> 46 | 47 | (0) 48 | 49 | 50 | 51 | 1->2 52 | 53 | 54 | 55 | 56 | 57 | 3 58 | 59 | 60 | reduce (+) 61 | 62 | <n> 63 | 64 | 65 | 66 | 1->3 67 | 68 | 69 | 70 | 71 | 72 | 4 73 | 74 | 75 | transpose(Ø) 76 | 77 | <m n> 78 | 79 | 80 | 81 | 3->4 82 | 83 | 84 | 85 | 86 | 87 | 5 88 | 89 | 90 | + 91 | 92 | <n m> 93 | 94 | 95 | 96 | 4->5 97 | 98 | 99 | 100 | 101 | 102 | 6 103 | 104 | 105 | Array A 106 | 107 | <n m> 108 | 109 | 110 | 111 | 5->6 112 | 113 | 114 | 115 | 116 | 117 | 7 118 | 119 | 120 | Array B 121 | 122 | <k l> 123 | 124 | 125 | 126 | 5->7 127 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /docs/implementation.rst: -------------------------------------------------------------------------------- 1 | Implementation 2 | ============== 3 | 4 | .. tikz:: MOA Compiler Steps 5 | :libs: positioning, shapes, fit, backgrounds 6 | :include: figures/moa-implementation.tikz 7 | :stringsubst: 8 | 9 | Every effort has been made to restrict the data structures and 10 | algorithms used in the implementation. For this work only 11 | tuples(namedtuples), dictionaries, and enums have been used. All of 12 | which map to low level data structures. Parts of MOA that are not 13 | involved directly with compilation may use more complex structures to 14 | appear ``pythonic``. 15 | 16 | ``python-moa`` is both a numeric and symbolic compiler. It is numeric 17 | whenever possible and resorts to symbolic expressions only when the 18 | value is not known at compile time. Examples of when ``python-moa`` 19 | must be symbolic is when the shape of an array is not known 20 | :math:`\shape A = \vcc2n`. All unknown symbols are represented as 21 | scalar arrays for example the array ``A`` above would be represented 22 | in the symbol table as follows. This means that once a value becomes 23 | symbolic it is inside a nested tuple structure. Allowing it to become 24 | compatible with the internal representation. 25 | 26 | .. code-block:: python 27 | 28 | symbol_table = { 29 | 'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (2, ast.Node((ast.NodeSymbol.ARRAY,), (), ('n',), ())), None, None)), 30 | 'n': ast.SymbolNode(ast.NodeSymbol.ARRAY, (), None, None) 31 | } 32 | 33 | 34 | Everything in MOA has a shape. Everything. Scalars, vectors, 35 | n-dimensional arrays, operations on arrays, and even functions. Thus 36 | shape are included with each node in the abstract syntax tree. 37 | 38 | Symbol Table 39 | ------------ 40 | 41 | A symbol table is used to keep track of all arrays in the abstract 42 | syntax tree. 43 | 44 | .. code-block:: python 45 | 46 | symbol_table = { 47 | 'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (2, 3), (1, 2, 3, 4, 5, 6)), 48 | '_i1': ast.SymbolNode(ast.NodeSymbol.INDEX, (), (0, 5, 1), 49 | '_a2': ast.SymbolNode(ast.NodeSymbol.ARRAY, (2,), (1, ast.Node((NodeSymbol.ARRAY,), (), ('_i1',), ()))) 50 | } 51 | 52 | At this present moment not much work is done to garbage collect the 53 | tree as reductions take place. 54 | 55 | 56 | 57 | Abstract Syntax Tree 58 | -------------------- 59 | 60 | The abstract syntax tree takes inspiration from lisp where each node 61 | is a tuple with the first node determining the ``node_type`` by an 62 | enum. There are three main types of nodes for the frontend. These 63 | types are ``array``, ``unary operation``, and ``binary 64 | operation``. Originally plain tuples were used to represent the nodes 65 | however this lead to ugly ``node[2][1]`` syntax. Using namedtuples 66 | allowed a similar experience with ``node.right_node.shape``. The 67 | abstract syntax tree is heavily tied to the symbol table. 68 | 69 | For example the following moa expression :math:`\vc0 \psi \transpose 70 | (A + B)` can be represented with the following symbol table and ast. 71 | 72 | .. 73 | .. doctest:: 74 | 75 | >>> {'A': SymbolNode(MOANodeTypes.ARRAY, None, None), 76 | ... 'B': SymbolNode(MOANodeTypes.ARRAY, None, None), 77 | ... '_a0': SymbolNode(MOANodeTypes.ARRAY, (1,), (0,))} 78 | {'A': SymbolNode(node_type=, shape=None, value=None), 'B': SymbolNode(node_type=, shape=None, value=None), '_a0': SymbolNode(node_type=, shape=(1,), value=(0,))} 79 | 80 | .. 81 | .. doctest:: 82 | 83 | >>> BinaryNode(MOANodeTypes.PSI, None, 84 | ... ArrayNode(MOANodeTypes.ARRAY, None, '_a0'), 85 | ... UnaryNode(MOANodeTypes.TRANSPOSE, None, 86 | ... BinaryNode(MOANodeTypes.PLUS, None, 87 | ... ArrayNode(MOANodeTypes.ARRAY, None, 'A'), 88 | ... ArrayNode(MOANodeTypes.ARRAY, None, 'B')))) 89 | BinaryNode(node_type=, shape=None, left_node=ArrayNode(node_type=, shape=None, symbol_node='_a0'), right_node=UnaryNode(node_type=, shape=None, right_node=BinaryNode(node_type=, shape=None, left_node=ArrayNode(node_type=, shape=None, symbol_node='A'), right_node=ArrayNode(node_type=, shape=None, symbol_node='B')))) 90 | 91 | 92 | Array 93 | +++++ 94 | 95 | Tuple representation ``ArrayNode(type, shape, name, value)`` 96 | 97 | Create array named A with shape (1, 3) values (1, 2, 3) 98 | 99 | .. 100 | .. doctest:: 101 | 102 | >>> ast.Node((ast.NodeSymbol.ARRAY,), (1, 3), ("A",), ()) 103 | Node(symbol=(,), shape=(1, 3), attrib=('A',), ()) 104 | 105 | Create array without name and unknown values 106 | 107 | .. 108 | .. doctest:: 109 | 110 | >>> ast.Node((ast.NodeSymbol.ARRAY,), (1, 3), ("_a0",), ()) 111 | Node(symbol=(,), shape=(1, 3), attrib=('_a0',), ()) 112 | 113 | 114 | Unary Operation 115 | +++++++++++++++ 116 | 117 | Unary representation ``UnaryNode(type, shape, right_node)`` 118 | 119 | Available unary operations: ``PLUSRED``, ``MINUSRED``, ``TIMESRED``, 120 | ``DIVIDERED``, ``IOTA``, ``DIM``, ``TAU``, ``SHAPE``, ``RAV``, 121 | ``TRANSPOSE``. 122 | 123 | .. 124 | .. doctest:: 125 | 126 | >>> UnaryNode(MOANodeTypes.TRANSPOSE, (3, 1), 127 | ... ArrayNode(MOANodeTypes.ARRAY, (1, 3), "A")) 128 | UnaryNode(node_type=, shape=(3, 1), right_node=ArrayNode(node_type=, shape=(1, 3), symbol_node='A')) 129 | 130 | Binary Operation 131 | ++++++++++++++++ 132 | 133 | Binary representation ``BinaryNode(type, shape, left_node, right_node)`` 134 | 135 | Available binary operations: ``PLUS``, ``MINUS``, ``TIMES``, 136 | ``DIVIDE``, ``PSI``, ``TAKE``, ``DROP``, ``CAT``, ``TRANSPOSEV``. 137 | 138 | .. 139 | .. doctest:: 140 | 141 | >>> BinaryNode(MOANodeTypes.PLUS, (2, 3), 142 | ... ArrayNode(MOANodeTypes.ARRAY, (), "A"), 143 | ... ArrayNode(MOANodeTypes.ARRAY, (2, 3), "B")) 144 | BinaryNode(node_type=, shape=(2, 3), left_node=ArrayNode(node_type=, shape=(), symbol_node='A'), right_node=ArrayNode(node_type=, shape=(2, 3), symbol_node='B')) 145 | 146 | Symbol Table 147 | ------------ 148 | 149 | More work need to be done on unknown shape fixed dimension before 150 | writing. 151 | 152 | Shape Calculation 153 | ----------------- 154 | 155 | Shape calculation can be done with a single pass post-order traversal 156 | (left, right, root) node. 157 | 158 | How shapes are calculated for given types. 159 | 160 | Array 161 | +++++ 162 | 163 | For now the shape of an array is required to be defined on the node 164 | and cannot be computed from another value. Thus the second argument 165 | (shape) cannot be ``None``. 166 | 167 | .. code-block:: python 168 | 169 | ArrayNode(MOANodeTypes.ARRAY, (2, 3), None, None)) 170 | 171 | Transpose 172 | +++++++++ 173 | 174 | Transpose has two forms a unary and binary definition. 175 | 176 | .. math:: 177 | 178 | \transpose A = (\reverse \iota \dims A) \transpose A 179 | 180 | For the simple case of the unary operator. 181 | 182 | 183 | Reduction 184 | --------- 185 | 186 | Reduction can be done with a single pass pre-order traversal with 187 | multiple replacements on each node (root, left, right) node. These 188 | replacements have the Church-Rosser property meaning that when 189 | applying reductions the ordering of the replacements does not change 190 | the final result. 191 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to uarray's documentation! 2 | ================================== 3 | 4 | `python-moa `_ is an ambitious 5 | project to provide a high level array operation interface that can 6 | compile down into low level machine code. In addition moa can 7 | provide predictions on the runtime and performance of a computation. 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | introduction 14 | theory 15 | implementation 16 | roadmap 17 | comparison 18 | optimization 19 | publications 20 | 21 | 22 | Indices and tables 23 | ================== 24 | 25 | * :ref:`genindex` 26 | * :ref:`modindex` 27 | * :ref:`search` 28 | -------------------------------------------------------------------------------- /docs/introduction.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | Python-moa (mathematics of arrays) is an approach to a high level 5 | tensor compiler that is based on the work of `Lenore Mullin 6 | `_ and her 7 | `dissertation 8 | `_. A 9 | high level compiler is necessary because there are many optimizations 10 | that a low level compiler such as ``gcc`` will miss. It is trying to 11 | solve many of the same problems as other technologies such as the 12 | `taco compiler `_ and the `xla compiler 13 | `_. However, it takes a much different 14 | approach than others guided by the following principles. 15 | 16 | 1. What is the shape? Everything has a shape. scalars, vectors, arrays, operations, and functions. 17 | 18 | 2. What are the given indicies and operations required to produce a given index in the result? 19 | 20 | Having a compiler that is guided upon these principles allows for high 21 | level reductions that other compilers will miss and allows for 22 | optimization of algorithms as a whole. Keep in mind that MOA is 23 | **NOT** a compiler. It is a theory that guides compiler 24 | development. Since `python-moa 25 | `_ is based on theory we 26 | get unique properties that other compilers cannot guarantee. 27 | 28 | - No out of bounds array accesses 29 | - A computation is determined to be either valid or invalid at compile time 30 | - The computation will always reduce to a deterministic minimal form (dnf) 31 | (see `church-rosser 32 | `_ 33 | property) 34 | - All MOA operations are composable (including black box functions 35 | and `gufuncs 36 | `_) 37 | - Arbitrary high level operations will compile down to a minimal 38 | backend instruction set. If the shape and indexing of a given 39 | operation is known it can be added to python-moa. 40 | 41 | Frontend 42 | -------- 43 | 44 | Lenore Mullin originally developed a `moa compiler 45 | `_ in the 90s with 46 | programs that used a symbolic syntax heavily inspired by `APL 47 | `_ (`example 48 | program 49 | `_). This 50 | work was carried into python-moa initially with a lex/yacc compiler 51 | with an example program below. 52 | 53 | .. code-block:: python 54 | 55 | from moa.frontend import parser 56 | 57 | context = parser.parse('<0> psi (+red (tran (A ^ + B ^ )))') 58 | 59 | Upon pursuing this approach it became apparent that MOA should not 60 | require that a new syntax be developed since it is only a theory! So a 61 | pythonic interface to MOA was developed that expressed the same ideas 62 | which look much like the current numeric python libraries. Ideally MOA 63 | is hidden from the user. The python-moa compiler is broken into 64 | several pieces each which their own responsibilities: shape, DNF, and 65 | ONF. 66 | 67 | .. code-block:: python 68 | 69 | from moa.frontend import LazyArray 70 | 71 | A = LazyArray(shape=('n', 'm'), name='A') 72 | B = LazyArray(shape=('k', 'l'), name='B') 73 | 74 | expression = (A + B).T.reduce('+')[0] 75 | expression 76 | 77 | .. image:: ./images/frontend.svg 78 | 79 | Shape Calculation 80 | ----------------- 81 | 82 | The shape calculation is responsible for calculating the shape at 83 | every step of the computation. This means that operations have a 84 | shape. Note that the compiler handles symbolic shapes thus the exact 85 | shape does not need to be known, only the dimension. After the shape 86 | calculation step we can guarantee that an algorithm is a valid program 87 | and there will be no out of bound memory accesses. Making MOA 88 | extremely compelling for `FPGAs 89 | `_ and 90 | compute units with a minimal OS. If an algorithm makes it past this 91 | stage and fails then it is an issue with the compiler not the 92 | algorithm. 93 | 94 | .. code-block:: python 95 | 96 | >>> expression.visualize(stage='shape') 97 | 98 | .. image:: ./images/shape.svg 99 | 100 | Denotational Normal Form (DNF) 101 | ------------------------------ 102 | 103 | The DNF's responsibility is to reduce the high level MOA expression to 104 | the minimal and optimal machine independent computation. This graph 105 | has all of the indexing patterns of the computation and resulting 106 | shapes. Notice that several operations disappear in this stage such a 107 | transpose because transpose is simply index manipulation. 108 | 109 | .. code-block:: python 110 | 111 | >>> expression.visualize(stage='dnf') 112 | 113 | .. image:: ./images/dnf.svg 114 | 115 | Operational Normal Form (ONF) 116 | ----------------------------- 117 | 118 | The ONF is the stage of the compiler that will have to be the most 119 | flexible. At its current stage the ONF is a naive compiler that does 120 | not perform many important optimizations such as `PSI reduction 121 | `_ 122 | which reduces the number of loops in the calculation, loop ordering, 123 | and minimize the number of accumulators. MOA has ideas of dimension 124 | lifting which make parallization and optimizing for cache sizes much 125 | easier. Additionally algorithms must be implemented differently for 126 | sparse, column major, row major. The ONF stage is responsible for any 127 | "optimal" machine dependent implementation. "optimal" will vary from 128 | user to user and thus will have to allow for multiple programs: 129 | optimal single core, optimal parallel, optimal gpu, optimal low 130 | memory, etc. 131 | 132 | .. code-block:: python 133 | 134 | >>> print(expression.compile(use_numba=True, include_conditions=False)) 135 | @numba.jit 136 | def f(A, B): 137 | n = A.shape[0] 138 | m = A.shape[1] 139 | k = B.shape[0] 140 | l = B.shape[1] 141 | 142 | _a21 = numpy.zeros(()) 143 | _a19 = numpy.zeros(()) 144 | 145 | _a21 = 0 146 | for _i10 in range(0, m, 1): 147 | _a21 = (_a21 + (A[(0, _i10)] + B[(0, _i10)])) 148 | _a19[()] = _a21 149 | return _a19 150 | 151 | 152 | Performance 153 | ----------- 154 | 155 | MOA excels at performing reductions and reducing the amount of actual 156 | work done. You will see that the following algorithm only requires the 157 | first index of the computation. Making the naive implementation 158 | ``1000x`` more expensive for ``1000x1000`` shaped array. The following 159 | benchmarks have been performed on my laptop with an intel 160 | i5-4200U. However, more benchmarks are always available on the `Travis 161 | CI `_ (these 162 | benchmarks test python-moa's weaknesses). You will see with the 163 | benchmarks that if **any** indexing is required MOA will be 164 | significantly faster unless you hand optimize the numerical 165 | computations. 166 | 167 | .. code-block:: python 168 | 169 | import numpy 170 | import numba 171 | 172 | n, m = 1000, 1000 173 | 174 | exec(expression.compile(backend='python', use_numba=True, include_conditions=False)) 175 | 176 | A = numpy.random.random((n, m)) 177 | B = numpy.random.random((n, m)) 178 | 179 | Here we execute the MOA optimized code with the help of `numba 180 | `_ which is a JIT LLVM compiler for 181 | python. 182 | 183 | .. code-block:: python 184 | 185 | %%timeit 186 | 187 | >>> f(A=A, B=B) 188 | 2.21 µs ± 36.6 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each) 189 | 190 | The following numpy computation is obviously the worst case expression 191 | that you could write but this brings up the point that often times the 192 | algorithm is expressed differently than the implementation. This is 193 | one of the problems that MOA hopes to solve. 194 | 195 | .. code-block:: python 196 | 197 | %%timeit 198 | 199 | >>> (A + B).T.sum(axis=0)[0] 200 | 2.68 ms ± 127 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) 201 | 202 | We notice that even with the optimized version MOA is faster. This is 203 | mostly due to the transpose operation the numpy performs that we have 204 | no way around. 205 | 206 | .. code-block:: python 207 | 208 | %%timeit 209 | 210 | >>> (A[0] + B[0]).T.sum(axis=0) 211 | 6.92 µs ± 157 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each) 212 | 213 | Conclusion 214 | ---------- 215 | 216 | I hope that this walk through has shown the promising results that the 217 | MOA theory can bring to tensor computations and the python ecosystem 218 | as a whole. Please feel free to try out the project at `Quansight 219 | Labs/python-moa `_. I 220 | hope that this work can allow for the analysis and optimization of 221 | algorithms in a mathematically rigorous way which allows users to 222 | express their algorithms in an implementation independent manner. 223 | -------------------------------------------------------------------------------- /docs/optimization.rst: -------------------------------------------------------------------------------- 1 | Machine Independent Optimization 2 | ================================ 3 | 4 | TODO 5 | 6 | Machine Dependent Optimization 7 | ============================== 8 | 9 | `python-moa` has the ability to have multiple backends. Generally 10 | these optimizations fall under loop optimizations. Wikipedia has a 11 | good overview approaches are `polyhedral and unimodular transformation 12 | `_. These issues of 13 | optimization fall outside of the scope of work of MOA and we need to 14 | learn how to use other's approaches. Some of the optimizations that we 15 | could expect the framework to perform. 16 | 17 | - loop fusion 18 | - loop ordering 19 | - loop reduction (psi reduction) 20 | - reorder expressions (distributive property +-*/) 21 | - dimension lifting for parallelizing algorithms 22 | - cache intermediate results (immediately obvious in multiple matrix multiply and fast fourier transform) 23 | 24 | Some frameworks claim to handle pieces of this problem so they should be looked into. 25 | 26 | - `loopy `_ 27 | - `mlir `_ 28 | - `polly `_ (looks promising) 29 | -------------------------------------------------------------------------------- /docs/papers/ARRAYS-2019/README.md: -------------------------------------------------------------------------------- 1 | # ARRAY 2019 2 | 3 | A two page extended abstract was written for the [PLDI 4 | ARRAY](https://pldi19.sigplan.org/series/ARRAY) series. 5 | 6 | ## Requirements 7 | 8 | Submissions are welcome in two categories: full papers and extended 9 | abstracts. All submissions should be formatted in conformance with the 10 | ACM SIGPLAN proceedings style. Accepted submissions in either category 11 | will be presented at the workshop. 12 | 13 | Extended abstracts may be up to 2pp; they may describe work in 14 | progress, tool demonstrations, and summaries of work published in full 15 | elsewhere. The focus of the extended abstract should be to explain why 16 | the proposed presentation will be of interest to the ARRAY 17 | audience. Submissions will be lightly reviewed only for relevance to 18 | the workshop, and will not published in the DL. 19 | 20 | Whether full papers or extended abstracts, submissions must be in PDF 21 | format, printable in black and white on US Letter sized paper. Papers 22 | must adhere to the standard SIGPLAN conference format: two columns, 23 | nine-point font on a ten-point baseline, with columns 20pc (3.33in) 24 | wide and 54pc (9in) tall, with a column gutter of 2pc (0.33in). A 25 | suitable document template for LaTeX is available at 26 | http://www.sigplan.org/Resources/Author/. 27 | 28 | Papers must be submitted using EasyChair. 29 | 30 | Authors take note: The official publication date of full papers is the 31 | date the proceedings are made available in the ACM Digital 32 | Library. This date may be up to two weeks prior to the workshop. The 33 | official publication date affects the deadline for any patent filings 34 | related to published work. 35 | 36 | ## Development 37 | 38 | Simply use [nix](https://nixos.org/nix/download.html) for building. 39 | 40 | ```shell 41 | nix-build 42 | okular result # open pdf with viewer 43 | ``` 44 | 45 | Or build it manually you will need texlive and xelatex installed. 46 | 47 | ```shell 48 | latexmk -xelatex paper.tex 49 | ``` 50 | 51 | -------------------------------------------------------------------------------- /docs/papers/ARRAYS-2019/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import { }}: 2 | 3 | pkgs.stdenv.mkDerivation { 4 | name = "python-moa-array-2019"; 5 | 6 | src = ./.; 7 | 8 | buildInputs = with pkgs; [ 9 | texlive.combined.scheme-full 10 | ]; 11 | 12 | buildPhase = '' 13 | latexmk -xelatex paper.tex 14 | ''; 15 | 16 | installPhase = '' 17 | cp paper.pdf $out 18 | ''; 19 | } 20 | -------------------------------------------------------------------------------- /docs/papers/ARRAYS-2019/paper.tex: -------------------------------------------------------------------------------- 1 | %% For double-blind review submission, w/o CCS and ACM Reference (max submission space) 2 | \documentclass[sigplan,review,anonymous]{acmart}\settopmatter{printfolios=true,printccs=false,printacmref=false} 3 | %% For double-blind review submission, w/ CCS and ACM Reference 4 | %\documentclass[sigplan,review,anonymous]{acmart}\settopmatter{printfolios=true} 5 | %% For single-blind review submission, w/o CCS and ACM Reference (max submission space) 6 | %\documentclass[sigplan,review]{acmart}\settopmatter{printfolios=true,printccs=false,printacmref=false} 7 | %% For single-blind review submission, w/ CCS and ACM Reference 8 | %\documentclass[sigplan,review]{acmart}\settopmatter{printfolios=true} 9 | %% For final camera-ready submission, w/ required CCS and ACM Reference 10 | %\documentclass[sigplan]{acmart}\settopmatter{} 11 | 12 | 13 | %% Conference information 14 | %% Supplied to authors by publisher for camera-ready submission; 15 | %% use defaults for review submission. 16 | \acmConference[PL'19]{ACM SIGPLAN Conference on Programming Languages}{January 01--03, 2018}{New York, NY, USA} 17 | \acmYear{2019} 18 | \acmISBN{} % \acmISBN{978-x-xxxx-xxxx-x/YY/MM} 19 | \acmDOI{} % \acmDOI{10.1145/nnnnnnn.nnnnnnn} 20 | \startPage{1} 21 | 22 | %% Copyright information 23 | %% Supplied to authors (based on authors' rights management selection; 24 | %% see authors.acm.org) by publisher for camera-ready submission; 25 | %% use 'none' for review submission. 26 | \setcopyright{none} 27 | %\setcopyright{acmcopyright} 28 | %\setcopyright{acmlicensed} 29 | %\setcopyright{rightsretained} 30 | %\copyrightyear{2018} %% If different from \acmYear 31 | 32 | %% Bibliography style 33 | \bibliographystyle{ACM-Reference-Format} 34 | %% Citation style 35 | %\citestyle{acmauthoryear} %% For author/year citations 36 | %\citestyle{acmnumeric} %% For numeric citations 37 | %\setcitestyle{nosort} %% With 'acmnumeric', to disable automatic 38 | %% sorting of references within a single citation; 39 | %% e.g., \cite{Smith99,Carpenter05,Baker12} 40 | %% rendered as [14,5,2] rather than [2,5,14]. 41 | %\setcitesyle{nocompress} %% With 'acmnumeric', to disable automatic 42 | %% compression of sequential references within a 43 | %% single citation; 44 | %% e.g., \cite{Baker12,Baker14,Baker16} 45 | %% rendered as [2,3,4] rather than [2-4]. 46 | 47 | 48 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 49 | %% Note: Authors migrating a paper from traditional SIGPLAN 50 | %% proceedings format to PACMPL format must update the 51 | %% '\documentclass' and topmatter commands above; see 52 | %% 'acmart-pacmpl-template.tex'. 53 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 54 | 55 | 56 | %% Some recommended packages. 57 | \usepackage{booktabs} %% For formal tables: 58 | %% http://ctan.org/pkg/booktabs 59 | \usepackage{subcaption} %% For complex figures with subfigures/subcaptions 60 | %% http://ctan.org/pkg/subcaption 61 | 62 | 63 | \begin{document} 64 | 65 | %% Title information 66 | \title[Short Title]{Full Title} %% [Short Title] is optional; 67 | %% when present, will be used in 68 | %% header instead of Full Title. 69 | \titlenote{with title note} %% \titlenote is optional; 70 | %% can be repeated if necessary; 71 | %% contents suppressed with 'anonymous' 72 | \subtitle{Subtitle} %% \subtitle is optional 73 | \subtitlenote{with subtitle note} %% \subtitlenote is optional; 74 | %% can be repeated if necessary; 75 | %% contents suppressed with 'anonymous' 76 | 77 | 78 | %% Author information 79 | %% Contents and number of authors suppressed with 'anonymous'. 80 | %% Each author should be introduced by \author, followed by 81 | %% \authornote (optional), \orcid (optional), \affiliation, and 82 | %% \email. 83 | %% An author may have multiple affiliations and/or emails; repeat the 84 | %% appropriate command. 85 | %% Many elements are not rendered, but should be provided for metadata 86 | %% extraction tools. 87 | 88 | %% Author with single affiliation. 89 | \author{Chris Ostrouchov} 90 | %% \authornote{with author1 note} %% \authornote is optional; 91 | %% can be repeated if necessary 92 | \orcid{0000-0002-8734-4564} %% \orcid is optional 93 | \affiliation{ 94 | \position{Scientific Software Developer} 95 | \department{Quansight Labs} %% \department is recommended 96 | \institution{Quansight} %% \institution is required 97 | %% \streetaddress{} 98 | %% \city{City1} 99 | %% \state{State1} 100 | %% \postcode{Post-Code1} 101 | \country{United States} %% \country is recommended 102 | } 103 | \email{costrouchov@quansight.com} %% \email is recommended 104 | 105 | %% Author with two affiliations and emails. 106 | \author{Lenore Mullin} 107 | %% \authornote{with author2 note} %% \authornote is optional; 108 | %% can be repeated if necessary 109 | \orcid{0000-0002-3339-6393} %% \orcid is optional 110 | \affiliation{ 111 | \position{Position2a} 112 | \department{Department2a} %% \department is recommended 113 | \institution{Institution2a} %% \institution is required 114 | \streetaddress{Street2a Address2a} 115 | \city{City2a} 116 | \state{State2a} 117 | \postcode{Post-Code2a} 118 | \country{Country2a} %% \country is recommended 119 | } 120 | \email{first2.last2@inst2a.com} %% \email is recommended 121 | \affiliation{ 122 | \position{Position2b} 123 | \department{Department2b} %% \department is recommended 124 | \institution{Institution2b} %% \institution is required 125 | \streetaddress{Street3b Address2b} 126 | \city{City2b} 127 | \state{State2b} 128 | \postcode{Post-Code2b} 129 | \country{Country2b} %% \country is recommended 130 | } 131 | \email{first2.last2@inst2b.org} %% \email is recommended 132 | 133 | 134 | %% Abstract 135 | %% Note: \begin{abstract}...\end{abstract} environment must come 136 | %% before \maketitle command 137 | \begin{abstract} 138 | Text of abstract \ldots. 139 | \end{abstract} 140 | 141 | 142 | %% 2012 ACM Computing Classification System (CSS) concepts 143 | %% Generate at 'http://dl.acm.org/ccs/ccs.cfm'. 144 | \begin{CCSXML} 145 | 146 | 147 | 10011007.10011006.10011008 148 | Software and its engineering~General programming languages 149 | 500 150 | 151 | 152 | 10003456.10003457.10003521.10003525 153 | Social and professional topics~History of programming languages 154 | 300 155 | 156 | 157 | \end{CCSXML} 158 | 159 | \ccsdesc[500]{Software and its engineering~General programming languages} 160 | \ccsdesc[300]{Social and professional topics~History of programming languages} 161 | %% End of generated code 162 | 163 | 164 | %% Keywords 165 | %% comma separated list 166 | \keywords{compiler} %% \keywords are mandatory in final camera-ready submission 167 | 168 | 169 | %% \maketitle 170 | %% Note: \maketitle command must come after title commands, author 171 | %% commands, abstract environment, Computing Classification System 172 | %% environment and commands, and keywords command. 173 | \maketitle 174 | 175 | 176 | \section{Introduction} 177 | 178 | Text of paper \ldots 179 | 180 | 181 | %% Acknowledgments 182 | \begin{acks} %% acks environment is optional 183 | %% contents suppressed with 'anonymous' 184 | %% Commands \grantsponsor{}{}{} and 185 | %% \grantnum[]{}{} should be used to 186 | %% acknowledge financial support and will be used by metadata 187 | %% extraction tools. 188 | This material is based upon work supported by the 189 | \grantsponsor{GS100000001}{National Science 190 | Foundation}{http://dx.doi.org/10.13039/100000001} under Grant 191 | No.~\grantnum{GS100000001}{nnnnnnn} and Grant 192 | No.~\grantnum{GS100000001}{mmmmmmm}. Any opinions, findings, and 193 | conclusions or recommendations expressed in this material are those 194 | of the author and do not necessarily reflect the views of the 195 | National Science Foundation. 196 | \end{acks} 197 | 198 | 199 | %% Bibliography 200 | %\bibliography{bibfile} 201 | 202 | 203 | %% Appendix 204 | \appendix 205 | \section{Appendix} 206 | 207 | Text of appendix \ldots 208 | 209 | \end{document} 210 | -------------------------------------------------------------------------------- /docs/papers/JOSS-2019/README.md: -------------------------------------------------------------------------------- 1 | # Journal of Open Source Software (JOSS) 2019 2 | 3 | Place for eventual JOSS submission 4 | 5 | # [Requirements](https://joss.readthedocs.io/en/latest/submitting.html) 6 | -------------------------------------------------------------------------------- /docs/presentations/scipy-2019-lightning/README.md: -------------------------------------------------------------------------------- 1 | # Scipy 2019 - Lightning Talk 2 | 3 | Quick talk about features of MOA to prepare for pydata talk 4 | -------------------------------------------------------------------------------- /docs/presentations/scipy-2019-lightning/presentation.md: -------------------------------------------------------------------------------- 1 | # Mathematics of Arrays (MOA) 2 | -------------------------------------------------------------------------------- /docs/publications.rst: -------------------------------------------------------------------------------- 1 | Publications 2 | ============ 3 | 4 | 5 | 1. Mullin, Lenore, "A Mathematics of Arrays", Thesis, December 1988 `link `_ 6 | 2. Mullin, Lenore, and Scott Thibault. "A Reduction semantics for array expressions: the PSI compiler." Department of Computer Science, University of Missouri-Rolla, Rolla, Missouri 65401 (1994). `link `_ 7 | -------------------------------------------------------------------------------- /docs/roadmap.rst: -------------------------------------------------------------------------------- 1 | Development Roadmap 2 | =================== 3 | 4 | Progress (in order) 5 | 6 | 1. Reduce 7 | 2. Modular compiler 8 | 3. Broadcasting 9 | 4. Psi reduction 10 | 11 | ... 12 | 13 | 14 | Reduce (binary operation) 15 | ------------------------- 16 | 17 | Reduction is at the heart of most linear algebra routines and 18 | computation in general. Thus this feature is extremely important to 19 | implement. So far the frontend, shape, dnf stages have been 20 | completed. Transforming the dnf to the onf is non-trivial with complex 21 | cases. Here I will present features that I believe need to be 22 | implemented in order for this to work. 23 | 24 | Suppose the following moa expression with the following shapes. 25 | 26 | - :math:`\shape A = \vcc34` 27 | - :math:`\shape B = \vc4` 28 | 29 | .. code-block:: text 30 | 31 | (B + +red A) + *red (A + A) 32 | 33 | 34 | .. code-block:: python 35 | 36 | _a1 = numpy.zeros((4,)) 37 | 38 | for _i0 in range(0, 4): 39 | _a2 = numpy.full((), 1) 40 | for _i1 in range(0, 3): 41 | _a2 = _a2 * (A[_i1, _i0] + A[_i1, _i0]) 42 | 43 | _a3 = numpy.full((), 0) 44 | for _i2 in range(0, 3): 45 | _a3 = _a3 + A[_i2, _i0] 46 | _a1[_i0] = B[_i0] + _a2 + _a3 47 | 48 | This is a simple moa expression to python code conversion but still 49 | several difficulties emerge. 50 | 51 | 1. with the expression ``_a1[i0] = B[i0] + _a2 + _a3`` the ``_a2, 52 | _a3`` are at different levels in the tree thus simple preorder 53 | traversal will not work. A custom tree traversal will be required 54 | (or postorder traversal might work?). **Will custom tree traversal 55 | be required?** 56 | 57 | 2. After reading the dragon book would it be advantageous to form a 58 | data flow graph? **Would data flow constructs be required for 59 | compiler?**. Already have ast nodes that can represent this block 60 | structure (multiple statements in one command). 61 | 62 | 63 | MOA Compiler 64 | ------------ 65 | 66 | Modularity to enable domain specific operations/shape/dnf 67 | reductions/onf optimizations. Why? Because broadcasting, ufuncs, 68 | slices are not within Lenore's thesis and a modular structure would 69 | allow user contributions to the moa engine and allow the core to 70 | be as minimal as possible. I would like to be able to enable/disable 71 | moa "features". This would also enforce good design. 72 | 73 | We need to clearly express what the "core" of moa is and express an 74 | easy way to make MOA modular. Thus at least a simple dispatch 75 | mechanism will be required for each stage? Here I am trying to express 76 | how I would expect modules would "plug in". 77 | 78 | Frontend 79 | ^^^^^^^^ 80 | 81 | .. code-block:: python 82 | 83 | A = LazyArray(shape=(3, 4), name='A', modules={ 84 | 'uarray.linalg', 'uarray.broadcasting', 'uarray.ufunc' 85 | }) 86 | B = LazyArray(shape=(3, 4), name='B') 87 | 88 | result = A.linalg.dot(B) 89 | 90 | result = A.core.add(B) 91 | 92 | result = A + B # (maps to A.core.add) 93 | 94 | Core would be a default required dependency but would be added in a 95 | "plug in" manner. Module extensions would be allowed to have 96 | "dependencies" which would allow "module" declarations to have 97 | multiple dependencies much like a package manager. 98 | 99 | These modules would add to each stage frontend, shape, and dnf. For 100 | example "broadcasting" would not affect the frontend but would allow 101 | for more possible compatible operations. 102 | 103 | Here I am showing how lazy array could express the modules that a user 104 | 105 | 106 | AST 107 | ^^^ 108 | 109 | Module system would at least require the ability for moa users to 110 | specify arbitrary node types (unique symbols). 111 | 112 | .. code-block:: python 113 | 114 | class MOACoreTypes(enum.Enum): 115 | PSI = (2, auto()) 116 | TRANSPOSE = (1, auto()) 117 | PLUS = (2, auto()) 118 | 119 | This would allow several enums to be used. For defining moa frontend, 120 | shape, dnf, onf symbols. 121 | 122 | Node(...) would use these modules to create the correct node_type 123 | contructor. Maybe some global state is needed? 124 | 125 | Maybe a tighter constraint is needed for nodes. 126 | 127 | ``(node_type, shape, *node_specific, *node_children)`` needs more 128 | thinking. 129 | 130 | Take for example an if statement ``(node_type, shape, condition_node, block)``. 131 | 132 | Maybe a block_type could be used and then all there would be no nested blocks! Looks like an important addition that would generalize well. 133 | 134 | Shape 135 | ^^^^^ 136 | 137 | .. code-block:: python 138 | 139 | calculate_shapes(symbol_table, tree, modules={'uarray.core'}) 140 | 141 | These modules would have a standardized way to specifying shape 142 | function mappings. Most likely a dictionary. 143 | 144 | Only one shape is allowed for each shape function. Thus one of two things must happen. 145 | 146 | - modules would override the shape function in order? Seems 147 | complicated but necessary in where multiple users have different 148 | implementations. 149 | - would throw an error stating that multiple functions are defined 150 | for a specific shape. 151 | 152 | ``(node_type, shape_function)`` 153 | 154 | DNF Reduction 155 | ^^^^^^^^^^^^^ 156 | 157 | Multiple possible reductions should not be possible for a given tree. Or should it? 158 | 159 | Need some type of reduction language for `(pattern, dnf_function)`. 160 | 161 | ONF Reduction 162 | ^^^^^^^^^^^^^ 163 | 164 | TODO need to think on this more. 165 | 166 | Numpy Broadcasting 167 | ------------------ 168 | 169 | Broadcasting is a technique that allows for mismatching shapes to 170 | apply an n-ary operation. 171 | 172 | Suppose the following ternary operation expression ``op`` with the 173 | following shapes. 174 | 175 | - :math:`\shape A = \vccc345` 176 | - :math:`\shape B = \vccc311` 177 | - :math:`\shape C = \vcccc2115` 178 | 179 | The resulting shape would be :math:`\vcccc2345`. 180 | 181 | This leads to the following indexing code. 182 | 183 | .. code-block:: python 184 | 185 | result[i, j, k, l] = A[j, k, l] + B[j, 0, 0] + C[i, 0, 0, l] 186 | 187 | The following rules define broadcasting. 188 | 189 | 1. For missing dimensions fill with ``1``s on the left hand side 190 | 191 | 2. Positional shape elements must all be equal or ``1``s 192 | 193 | For symbolic shapes this constraint leads to multiple possible code 194 | paths. Suppose. 195 | 196 | Suppose the following ternary operation expression ``op`` with the 197 | following shapes. 198 | 199 | - :math:`\shape A = \vccc345` 200 | - :math:`\shape B = \vccc3n1` 201 | 202 | ``n`` can equal either 1 or 4. 203 | 204 | .. code-block:: python 205 | 206 | # n = 1 207 | result[i, j, k] = A[i, j, k] + B[i, 0, 0] 208 | 209 | # n = 4 210 | result[i, j, k] = A[i, j, k] + B[i, j, 0] 211 | 212 | This will need to be solved for symbolic shapes. 213 | 214 | Also note that a max function will be required to determine shape. 215 | 216 | - :math:`\shape A = \vccc3m5` 217 | - :math:`\shape B = \vccc3n1` 218 | 219 | The resulting shape is ``k = max(n, m)`` :math:`\vccc3k4`. 220 | 221 | gufuncs 222 | ------------ 223 | 224 | gufuncs are the idea that a given operation ``f`` can take in ``n`` 225 | arrays with differing dimensions and output ``p`` arrays with 226 | differing dimensions. With this the input arrays "eat" the right most 227 | dimensions from the input arrays. The remaining left most dimensions 228 | of the arrays must be equal or satisfy some relaxed condition such as 229 | broadcasting or scalar extension in moa. 230 | 231 | So gufuncs are two ideas in moa. 232 | 233 | 1. ``f(A, B, C, ...) -> D, E, ...`` 234 | 2. ``omega`` which applies the given operation to the left most dimensions. 235 | 236 | Lets looks at an example and show that MOA is "almost" advocating broadcasting. 237 | 238 | Suppose a function ``g`` that takes input arguments 2 dimensional, 1 dimensional. 239 | 240 | And we have two input arrays. 241 | 242 | - :math:`\shape A = \vcccc2345` 243 | - :math:`\shape B = \vcc34` 244 | 245 | :math:`m = \min(4-2, 2-1) = 1` 246 | 247 | Then the only requirement is that :math:`\vc3 = \vc3`. Notice that 2 248 | is not in this. Lenore then implies that scalar extension also applies 249 | to omega. 250 | 251 | Lets consider a complex example. A tensor contraction with broadcasting. 252 | 253 | - :math:`\shape A = \vcccc945` 254 | - :math:`\shape B = \vcc56` 255 | - :math:`\shape C = \vccc946` 256 | 257 | We apply a tensor contraction to the innermost "right most" dimensions 258 | of A and B. With :math:`\emptyset` and :math:`\vc9`. This is 259 | broadcasted to :math:`\vc9`. With the resulting shape of C. 260 | 261 | 262 | Slice MOA Operation 263 | ------------------- 264 | 265 | Take, drop, and reverse are not general enough. How would one 266 | represent ``A[::2]``? Currently this is not possible but the shape is 267 | deterministic. Thus I recommend a slice operation ``A slice B`` where :math:`\shape A = \vccn3`. 268 | 269 | 270 | Conditional Indexing 271 | -------------------- 272 | 273 | Allowing a selection of elements to be taken from an array. The resulting shape. 274 | 275 | .. code-block:: python 276 | 277 | A[A > 5] 278 | 279 | This indexing will result in a vector. A corresponding reshape 280 | operation would be nice to have. 281 | 282 | 283 | Reshape 284 | ------- 285 | 286 | TODO 287 | 288 | PSI Reduction Implementation 289 | ---------------------------- 290 | 291 | Useful for a huge performance increase and reduce the number of 292 | loops. Need further understanding of this topic. 293 | -------------------------------------------------------------------------------- /docs/theory.rst: -------------------------------------------------------------------------------- 1 | Theory 2 | ====== 3 | 4 | Arrays 5 | ^^^^^^ 6 | 7 | In MOA everything is an array or operations on arrays. To help with 8 | discussion we will be using the following notation. More detailed 9 | 10 | Constants are defined as a zero dimensional array :math:`\emptyset` or :math:`< \;\; >` 11 | 12 | A **vector** :math:`\vccc123` is a a one dimensional array. 13 | 14 | A **multidimensional array** :math:`\aaccIcc1212`. Notice that 15 | brackets are used to designate a two dimensional array. We can build 16 | higher dimensional arrays by composing bracket and angled brackets 17 | together. 18 | 19 | .. warning:: 20 | 21 | :math:`\accc123` is NOT a vector it is an array of dimension two 22 | 23 | Shape :math:`\rho` rho 24 | ^^^^^^^^^^^^^^^^^^^^^^ 25 | 26 | The shape of a multidimensional array is an important concept. It is 27 | similar to thinking of the `shape` in `numpy 28 | `_. In 29 | moa the symbol for shape is :math:`\shape` and it is a unary operator on 30 | arrays. Let's look at the examples of arrays above and inspect their 31 | shapes. 32 | 33 | .. math:: 34 | 35 | \begin{align} 36 | \shape \emptyset & = \vc0 \\ 37 | \shape \vccc123 & = \vc3 \\ 38 | \shape \accc123 & = \vcc13 \\ 39 | \shape \avcc12 & = \vccc121 \\ 40 | \shape \aacc12 & = \vcccc1211 41 | \end{align} 42 | 43 | Dimension :math:`delta` delta 44 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 45 | 46 | We skipped ahead of ourselves earlier when we talked about the 47 | dimensionality of an array. For example we said that :math:`\vccc123` 48 | is a one dimensional array. How do we define this? Let us formally 49 | define the unary operation dimension :math:`\delta`. 50 | 51 | **Definition:** :math:`\delta A = \rho ( \rho A )` 52 | 53 | The following definition can rigorously define dimensionality even 54 | though it may appear trivial. For example :math:`\delta \vccc123 = 55 | \rho ( \rho \vccc123 ) = \rho \vc3 = \vc1`. 56 | 57 | Pi :math:`\pi` 58 | ^^^^^^^^^^^^^^ 59 | 60 | The following unary operation pi :math:`\pi` only applies to vectors 61 | (otherwise known as arrays of dimension one). This operation will be 62 | needed for future derivations. 63 | 64 | 1. :math:`\pi \emptyset = 1` 65 | 66 | 2. :math:`\pi \vcccc1234 = 24` 67 | 68 | Total :math:`\tau` tau 69 | ^^^^^^^^^^^^^^^^^^^^^^ 70 | 71 | It is from the following definition that we can define total 72 | :math:`tau` which is the total number of elements in an array. Detailed description can be found on 73 | 74 | **Definition:** :math:`\tau A = \pi ( \rho A )` 75 | 76 | Using this definition we develop the total number of elements in an 77 | array. For example :math:`\tau \vccc123 = \pi ( \shape \acccc1234 ) = 78 | \pi \vcc14 = 4`. 79 | 80 | 1. :math:`\dims \emptyset = \pi ( \shape \emptyset ) = \pi \vc0 = 0` 81 | 82 | 2. :math:`\dims \avcc{}{} = \pi ( \shape \avcc{}{} ) = \pi \vccc120 = 0` 83 | 84 | .. note:: 85 | 86 | There are an infinite number of empty arrays. This concept may seem 87 | weird at first. 88 | -------------------------------------------------------------------------------- /moa/__init__.py: -------------------------------------------------------------------------------- 1 | """Python Mathematics of Arrays (MOA) 2 | 3 | """ 4 | 5 | __version__ = "0.0.1" 6 | -------------------------------------------------------------------------------- /moa/analysis.py: -------------------------------------------------------------------------------- 1 | from functools import reduce 2 | 3 | from . import ast 4 | 5 | 6 | def metric_flops(context): 7 | def _count_flops(context): 8 | if context.ast.shape is None: 9 | raise ValueError('metric "flops" assumes that shape analysis is completed') 10 | 11 | flops = 0 12 | 13 | if context.ast.symbol[-1] in {ast.NodeSymbol.PLUS, ast.NodeSymbol.MINUS, ast.NodeSymbol.TIMES, ast.NodeSymbol.DIVIDE}: 14 | flops = reduce(lambda x,y: x*y, context.ast.shape) 15 | 16 | return ast.create_context( 17 | ast=flops + sum([_ for _ in context.ast.child]), 18 | symbol_table=context.symbol_table) 19 | 20 | context = ast.node_traversal(context, _count_flops, traversal='postorder') 21 | return context.ast 22 | -------------------------------------------------------------------------------- /moa/array.py: -------------------------------------------------------------------------------- 1 | """Pure python slow array "indexing" class. It is intentional that 2 | numpy is not used. This is an class that will try to implement the 3 | universal array interface with MOA in mind. Psi reduction will soon be 4 | implemented and I will be able to reduce the expectations of this 5 | class (specifically the psi method taking a tuple). All the dunder 6 | madness occurs from trying to treat scalar operations appropriately. 7 | 8 | Eventually this will turn into a lazy array moa interface. MOA needs 9 | to stabilize and more work before that happens. 10 | 11 | """ 12 | 13 | class Array: 14 | def __init__(self, shape, value=None, fmt='row'): 15 | total = 1 16 | for i in shape: 17 | total *= i 18 | 19 | if value: 20 | if total != len(value): 21 | raise ValueError('number of values must match shape') 22 | self.value = list(value) 23 | else: 24 | self.value = [0] * total 25 | 26 | self._shape = shape 27 | 28 | if fmt != 'row': 29 | raise NotImplementedError('only "row" based format implmented') 30 | self.fmt = fmt 31 | 32 | @property 33 | def shape(self): 34 | return self._shape 35 | 36 | def _offset(self, index): 37 | if len(index) != len(self.shape): 38 | raise IndexError('index is not a full index') 39 | 40 | stride = 1 41 | offset = 0 42 | for i, s in zip(index[::-1], self.shape[::-1]): 43 | if i >= s: 44 | raise IndexError(f'index {i} >= {s} is incompatible with shape') 45 | 46 | offset += stride * i 47 | stride *= s 48 | 49 | return offset 50 | 51 | def __getitem__(self, index): 52 | return self.value[self._offset(index)] 53 | 54 | def __setitem__(self, index, value): 55 | self.value[self._offset(index)] = value 56 | 57 | def __repr__(self): 58 | return f'Array(shape={self.shape}, value={self.value})' 59 | 60 | # scalar operations 61 | def __add__(self, other): 62 | if len(self.shape) != 0: 63 | raise TypeError('only scalar operations allowed') 64 | return self[()] + other 65 | 66 | def __radd__(self, other): 67 | if len(self.shape) != 0: 68 | raise TypeError('only scalar operations allowed') 69 | return other + self[()] 70 | 71 | def __sub__(self, other): 72 | if len(self.shape) != 0: 73 | raise TypeError('only scalar operations allowed') 74 | return self[()] - other 75 | 76 | def __rsub__(self, other): 77 | if len(self.shape) != 0: 78 | raise TypeError('only scalar operations allowed') 79 | return other - self[()] 80 | 81 | def __mul__(self, other): 82 | if len(self.shape) != 0: 83 | raise TypeError('only scalar operations allowed') 84 | return self[()] * other 85 | 86 | def __rmul__(self, other): 87 | if len(self.shape) != 0: 88 | raise TypeError('only scalar operations allowed') 89 | return other * self[()] 90 | 91 | def __truediv__(self, other): 92 | if len(self.shape) != 0: 93 | raise TypeError('only scalar operations allowed') 94 | return self[()] / other 95 | 96 | def __rtruediv__(self, other): 97 | if len(self.shape) != 0: 98 | raise TypeError('only scalar operations allowed') 99 | return other / self[()] 100 | 101 | # scalar comparison 102 | def __lt__(self, other): 103 | if len(self.shape) != 0: 104 | raise TypeError('only scalar operations allowed') 105 | return self[()] < other 106 | 107 | def __le__(self, other): 108 | if len(self.shape) != 0: 109 | raise TypeError('only scalar operations allowed') 110 | return self[()] <= other 111 | 112 | def __gt__(self, other): 113 | if len(self.shape) != 0: 114 | raise TypeError('only scalar operations allowed') 115 | return self[()] > other 116 | 117 | def __ge__(self, other): 118 | if len(self.shape) != 0: 119 | raise TypeError('only scalar operations allowed') 120 | return self[()] >= other 121 | 122 | def __eq__(self, other): 123 | if len(self.shape) != 0: 124 | raise TypeError('only scalar operations allowed') 125 | return self[()] == other 126 | 127 | def __ne__(self, other): 128 | if len(self.shape) != 0: 129 | raise TypeError('only scalar operations allowed') 130 | return self[()] != other 131 | -------------------------------------------------------------------------------- /moa/backend/__init__.py: -------------------------------------------------------------------------------- 1 | from .python import generate_python_source 2 | -------------------------------------------------------------------------------- /moa/backend/python.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import astunparse 3 | 4 | from ..ast import ( 5 | create_context, 6 | select_node, 7 | NodeSymbol, node_traversal, 8 | has_symbolic_elements, is_symbolic_element, 9 | select_array_node_symbol, 10 | ) 11 | 12 | 13 | def python_backend(context): 14 | """Convert MOA reduced AST to python source code 15 | 16 | """ 17 | context = node_traversal(context, _ast_replacement, traversal='postorder') 18 | return context.ast 19 | 20 | 21 | def generate_python_source(context, materialize_scalars=False, use_numba=False): 22 | python_ast = python_backend(context) 23 | 24 | class ReplaceScalars(ast.NodeTransformer): 25 | def visit_Name(self, node): 26 | symbol_node = context.symbol_table.get(node.id) 27 | if symbol_node is not None and symbol_node.symbol != NodeSymbol.INDEX and (symbol_node.shape == () and symbol_node.value is not None and not has_symbolic_elements(symbol_node.value)): 28 | return ast.Num(symbol_node.value[0]) 29 | return node 30 | 31 | # TODO: this will no longer be necissary with psi reduction 32 | class ReplaceShapeIndex(ast.NodeTransformer): 33 | def visit_Subscript(self, node): 34 | if isinstance(node.value, ast.Attribute) and node.value.attr == 'shape': 35 | return ast.Subscript(value=node.value, 36 | slice=ast.Index(value=node.slice.value.elts[0]), 37 | ctx=ast.Load()) 38 | return node 39 | 40 | class ReplaceWithNumba(ast.NodeTransformer): 41 | def visit_FunctionDef(self, node): 42 | node.decorator_list = [ast.Name(id='numba.jit', ctx=ast.Load())] 43 | self.generic_visit(node) 44 | return node 45 | 46 | def visit_Name(self, node): 47 | if node.id == 'Array': 48 | node.id = 'numpy.zeros' 49 | return node 50 | 51 | if materialize_scalars: 52 | python_ast = ReplaceScalars().visit(python_ast) 53 | python_ast = ReplaceShapeIndex().visit(python_ast) 54 | if use_numba: 55 | python_ast = ReplaceWithNumba().visit(python_ast) 56 | 57 | return astunparse.unparse(python_ast)[:-1] # remove newline 58 | 59 | 60 | def _ast_replacement(context): 61 | _NODE_AST_MAP = { 62 | (NodeSymbol.ARRAY,): _ast_array, 63 | (NodeSymbol.INDEX,): _ast_array, 64 | (NodeSymbol.FUNCTION,): _ast_function, 65 | (NodeSymbol.CONDITION,): _ast_condition, 66 | (NodeSymbol.BLOCK,): _ast_block, 67 | (NodeSymbol.ERROR,): _ast_error, 68 | (NodeSymbol.ASSIGN,): _ast_assignment, 69 | (NodeSymbol.INITIALIZE,): _ast_initialize, 70 | (NodeSymbol.LOOP,): _ast_loop, 71 | (NodeSymbol.SHAPE,): _ast_shape, 72 | (NodeSymbol.DIM,): _ast_dimension, 73 | (NodeSymbol.PSI,): _ast_psi, 74 | (NodeSymbol.PLUS,): _ast_plus_minus_times_divide, 75 | (NodeSymbol.MINUS,): _ast_plus_minus_times_divide, 76 | (NodeSymbol.TIMES,): _ast_plus_minus_times_divide, 77 | (NodeSymbol.DIVIDE,): _ast_plus_minus_times_divide, 78 | (NodeSymbol.EQUAL,): _ast_comparison_operations, 79 | (NodeSymbol.NOTEQUAL,): _ast_comparison_operations, 80 | (NodeSymbol.LESSTHAN,): _ast_comparison_operations, 81 | (NodeSymbol.LESSTHANEQUAL,): _ast_comparison_operations, 82 | (NodeSymbol.GREATERTHAN,): _ast_comparison_operations, 83 | (NodeSymbol.GREATERTHANEQUAL,): _ast_comparison_operations, 84 | (NodeSymbol.AND,): _ast_boolean_binary_operations, 85 | (NodeSymbol.OR,): _ast_boolean_binary_operations, 86 | (NodeSymbol.NOT,): _ast_boolean_unary_operations, 87 | } 88 | return _NODE_AST_MAP[context.ast.symbol](context) 89 | 90 | 91 | # helper 92 | def _ast_element(context, element): 93 | if is_symbolic_element(element): 94 | return _ast_replacement(create_context(ast=element, symbol_table=context.symbol_table)).ast 95 | else: 96 | return ast.Num(n=element) 97 | 98 | 99 | def _ast_tuple(context, value): 100 | return ast.Tuple(elts=[_ast_element(context, element) for element in value]) 101 | 102 | 103 | # python elements 104 | def _ast_psi(context): 105 | left_symbol_node = select_node(context, (0,)).ast.id 106 | return create_context( 107 | ast=ast.Subscript(value=select_node(context, (1,)).ast, 108 | slice=ast.Index(value=_ast_tuple(context, context.symbol_table[left_symbol_node].value)), 109 | ctx=ast.Load()), 110 | symbol_table=context.symbol_table) 111 | 112 | 113 | def _ast_array(context): 114 | return create_context( 115 | ast=ast.Name(id=context.ast.attrib[0], ctx=ast.Load()), 116 | symbol_table=context.symbol_table) 117 | 118 | 119 | def _ast_function(context): 120 | return create_context( 121 | ast=ast.FunctionDef(name='f', 122 | args=ast.arguments(args=[ast.arg(arg=arg, annotation=None) for arg in context.ast.attrib[0]], 123 | vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]), 124 | body=[ast.Expr(value=child_node) for child_node in context.ast.child] + [ast.Return(value=ast.Name(id=context.ast.attrib[1]))], 125 | decorator_list=[], 126 | returns=None), 127 | symbol_table=context.symbol_table) 128 | 129 | 130 | def _ast_assignment(context): 131 | return create_context( 132 | ast=ast.Assign(targets=[select_node(context, (0,)).ast], value=select_node(context, (1,)).ast), 133 | symbol_table=context.symbol_table) 134 | 135 | 136 | def _ast_loop(context): 137 | node_symbol = select_array_node_symbol(context) 138 | return create_context( 139 | ast=ast.For(target=ast.Name(id=context.ast.attrib[0]), 140 | iter=ast.Call(func=ast.Name(id='range'), 141 | args=[ 142 | _ast_element(context, node_symbol.value[0]), 143 | _ast_element(context, node_symbol.value[1]), 144 | _ast_element(context, node_symbol.value[2]) 145 | ], keywords=[]), body=select_node(context, (0,)).ast, orelse=[]), 146 | symbol_table=context.symbol_table) 147 | 148 | 149 | def _ast_error(context): 150 | return create_context( 151 | ast=ast.Raise(exc=ast.Call(func=ast.Name(id='Exception'), args=[ast.Str(s=context.ast.attrib[0])], keywords=[]), cause=None), 152 | symbol_table=context.symbol_table) 153 | 154 | 155 | def _ast_initialize(context): 156 | return create_context( 157 | ast=ast.Assign(targets=[ast.Name(id=context.ast.attrib[0])], 158 | value=ast.Call(func=ast.Name(id='Array'), 159 | args=[_ast_tuple(context, context.ast.shape)], 160 | keywords=[])), 161 | symbol_table=context.symbol_table) 162 | 163 | 164 | def _ast_shape(context): 165 | return create_context( 166 | ast=ast.Attribute(value=select_node(context, (0,)).ast, attr='shape', ctx=ast.Load()), 167 | symbol_table=context.symbol_table) 168 | 169 | 170 | def _ast_dimension(context): 171 | return create_context( 172 | ast=ast.Call(func=ast.Name(id='len', ctx=ast.Load()), args=[_ast_shape(context).ast], keywords=[]), 173 | symbol_table=context.symbol_table) 174 | 175 | 176 | def _ast_plus_minus_times_divide(context): 177 | binop_map = { 178 | (NodeSymbol.PLUS,): ast.Add, 179 | (NodeSymbol.MINUS,): ast.Sub, 180 | (NodeSymbol.TIMES,): ast.Mult, 181 | (NodeSymbol.DIVIDE,): ast.Div, 182 | } 183 | return create_context( 184 | ast=ast.BinOp(left=select_node(context, (0,)).ast, op=binop_map[context.ast.symbol](), right=select_node(context, (1,)).ast), 185 | symbol_table=context.symbol_table) 186 | 187 | 188 | def _ast_block(context): 189 | return create_context( 190 | ast=[ast.Expr(value=child_node) for child_node in context.ast.child], 191 | symbol_table=context.symbol_table) 192 | 193 | 194 | def _ast_condition(context): 195 | return create_context( 196 | ast=ast.If(test=select_node(context, (0,)).ast, body=select_node(context, (1,)).ast, orelse=[]), 197 | symbol_table=context.symbol_table) 198 | 199 | 200 | def _ast_comparison_operations(context): 201 | comparision_map = { 202 | (NodeSymbol.EQUAL,): ast.Eq, 203 | (NodeSymbol.NOTEQUAL,): ast.NotEq, 204 | (NodeSymbol.LESSTHAN,): ast.Lt, 205 | (NodeSymbol.LESSTHANEQUAL,): ast.LtE, 206 | (NodeSymbol.GREATERTHAN,): ast.Gt, 207 | (NodeSymbol.GREATERTHANEQUAL,): ast.GtE, 208 | } 209 | return create_context( 210 | ast=ast.Compare(left=select_node(context, (0,)).ast, 211 | ops=[comparision_map[context.ast.symbol]()], 212 | comparators=[select_node(context, (1,)).ast]), 213 | symbol_table=context.symbol_table) 214 | 215 | 216 | def _ast_boolean_binary_operations(context): 217 | boolean_map = { 218 | (NodeSymbol.AND,): ast.And, 219 | (NodeSymbol.OR,): ast.Or 220 | } 221 | return create_context( 222 | ast=ast.BoolOp(op=boolean_map[context.ast.symbol](), values=[ 223 | select_node(context, (0,)).ast, select_node(context, (1,)).ast]), 224 | symbol_table=context.symbol_table) 225 | 226 | 227 | def _ast_boolean_unary_operations(context): 228 | boolean_map = { 229 | (NodeSymbol.NOT,): ast.Not 230 | } 231 | return create_context( 232 | ast=ast.UnaryOp(op=boolean_map[context.ast.symbol](), operand=select_node(context, (0,)).ast), 233 | symbol_table=context.symbol_table) 234 | -------------------------------------------------------------------------------- /moa/compiler.py: -------------------------------------------------------------------------------- 1 | from moa.shape import calculate_shapes 2 | from moa.dnf import reduce_to_dnf 3 | from moa.onf import reduce_to_onf 4 | from moa.backend import generate_python_source 5 | 6 | 7 | def compiler(context, backend='python', include_conditions=True, use_numba=False): 8 | shape_context = calculate_shapes(context) 9 | dnf_context = reduce_to_dnf(shape_context) 10 | onf_context = reduce_to_onf(dnf_context, include_conditions=include_conditions) 11 | 12 | if backend == 'python': 13 | return generate_python_source(onf_context, materialize_scalars=True, use_numba=use_numba) 14 | else: 15 | raise ValueError(f'unknown backend {backend}') 16 | -------------------------------------------------------------------------------- /moa/exception.py: -------------------------------------------------------------------------------- 1 | class MOAException(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /moa/frontend/__init__.py: -------------------------------------------------------------------------------- 1 | from .array import LazyArray 2 | 3 | 4 | def parse(source, frontend='moa'): 5 | if frontend == 'moa': 6 | from .moa import MOAParser, MOALexer 7 | parser = MOAParser() 8 | context = parser.parse(source) 9 | else: 10 | raise ValueError(f'frontend "{frontend}" not implemented') 11 | 12 | return context 13 | -------------------------------------------------------------------------------- /moa/frontend/__main__.py: -------------------------------------------------------------------------------- 1 | from .moa import MOAParser 2 | from ..visualize import print_ast 3 | 4 | 5 | if __name__ == "__main__": 6 | parser = MOAParser() 7 | 8 | print('MOA Calculator') 9 | while True: 10 | text = input('>>> ') 11 | if text in {'q', 'quit'}: break 12 | print_ast(*parser.parse(text)) 13 | -------------------------------------------------------------------------------- /moa/frontend/array.py: -------------------------------------------------------------------------------- 1 | from .. import ast, compiler 2 | from ..shape import calculate_shapes 3 | from ..dnf import reduce_to_dnf 4 | from ..onf import reduce_to_onf 5 | from ..analysis import metric_flops 6 | from ..visualize import visualize_ast, print_ast 7 | 8 | 9 | class LazyArray: 10 | OPPERATION_MAP = { 11 | '+': ast.NodeSymbol.PLUS, 12 | '-': ast.NodeSymbol.MINUS, 13 | '*': ast.NodeSymbol.TIMES, 14 | '/': ast.NodeSymbol.DIVIDE, 15 | } 16 | 17 | def __init__(self, shape, value=None, name=None): 18 | if name is None and value is None: 19 | raise ValueError('either name or value must be supplied for LazyArray') 20 | 21 | if value is not None: 22 | raise NotImplemenetedError('not able to pass in value at compile time at this moment') 23 | 24 | self.context = ast.create_context() 25 | 26 | shape = self._create_array_from_list_tuple(shape) 27 | name = name or ast.generate_unique_array_name(self.context) 28 | 29 | self.context = ast.create_context( 30 | ast=ast.Node((ast.NodeSymbol.ARRAY,), None, (name,), ()), 31 | symbol_table=self.context.symbol_table) 32 | self.context = ast.add_symbol(self.context, name, ast.NodeSymbol.ARRAY, shape, None, value) 33 | 34 | def __getitem__(self, index): 35 | if isinstance(index, (int, str, slice)): 36 | index = (index,) 37 | 38 | indicies = () 39 | strides = () 40 | for i in index: 41 | if isinstance(i, str): 42 | if strides: 43 | raise IndexError('current limitation that indexing is not allowed past strides') 44 | self.context = ast.add_symbol(self.context, i, ast.NodeSymbol.ARRAY, (), None, None) 45 | indicies = indicies + (ast.Node((ast.NodeSymbol.ARRAY,), (), (i,), ()),) 46 | elif isinstance(i, int): 47 | if strides: 48 | raise IndexError('current limitation that indexing is not allowed past strides') 49 | indicies = indicies + (i,) 50 | elif isinstance(i, slice): 51 | strides = strides + ((i.start, i.stop, i.step),) 52 | else: 53 | raise IndexError('only integers, symbols, and slices (`:`) are valid indicies') 54 | 55 | if indicies: 56 | array_name = ast.generate_unique_array_name(self.context) 57 | self.context = ast.add_symbol(self.context, array_name, ast.NodeSymbol.ARRAY, (len(indicies),), None, indicies) 58 | self.context = ast.create_context( 59 | ast=ast.Node((ast.NodeSymbol.PSI,), None, (), (ast.Node((ast.NodeSymbol.ARRAY,), None, (array_name,), ()), self.context.ast)), 60 | symbol_table=self.context.symbol_table) 61 | if strides: 62 | raise NotImplemenetedError('strides not implemented') 63 | return self 64 | 65 | def _create_array_from_int_float_string(self, value): 66 | if isinstance(value, str): 67 | array_name = value 68 | self.context = ast.add_symbol(self.context, array_name, ast.NodeSymbol.ARRAY, (), None, None) 69 | else: 70 | array_name = ast.generate_unique_array_name(self.context) 71 | self.context = ast.add_symbol(self.context, array_name, ast.NodeSymbol.ARRAY, (), None, (value,)) 72 | return ast.Node((ast.NodeSymbol.ARRAY,), None, (array_name,), ()) 73 | 74 | def _create_array_from_list_tuple(self, value): 75 | if not any(isinstance(_, str) for _ in value): 76 | return value 77 | 78 | elements = () 79 | for element in value: 80 | if isinstance(element, str): 81 | self.context = ast.add_symbol(self.context, element, ast.NodeSymbol.ARRAY, (), None, None) 82 | elements = elements + (ast.Node((ast.NodeSymbol.ARRAY,), (), (element,), ()),) 83 | else: 84 | elements = elements + (element,) 85 | return elements 86 | 87 | @property 88 | def T(self): 89 | return self.transpose() 90 | 91 | def transpose(self, transpose_vector=None): 92 | if transpose_vector is None: 93 | self.context = ast.create_context( 94 | ast=ast.Node((ast.NodeSymbol.TRANSPOSE,), None, (), (self.context.ast,)), 95 | symbol_table=self.context.symbol_table) 96 | else: 97 | symbolic_vector = self._create_array_from_list_tuple(transpose_vector) 98 | 99 | array_name = ast.generate_unique_array_name(self.context) 100 | self.context = ast.add_symbol(self.context, array_name, ast.NodeSymbol.ARRAY, (len(symbolic_vector),), None, tuple(symbolic_vector)) 101 | self.context = ast.create_context( 102 | ast=ast.Node((ast.NodeSymbol.TRANSPOSEV,), None, (), (ast.Node((ast.NodeSymbol.ARRAY,), None, (array_name,), ()), self.context.ast)), 103 | symbol_table=self.context.symbol_table) 104 | return self 105 | 106 | def outer(self, operation, array): 107 | if operation not in self.OPPERATION_MAP: 108 | raise ValueError(f'operation {operation} not one of allowed operations {self.OPPERATION_MAP.keys()}') 109 | 110 | moa_operation = (ast.NodeSymbol.DOT, self.OPPERATION_MAP[operation]) 111 | 112 | if isinstance(array, self.__class__): 113 | new_symbol_table, left_context, right_context = ast.join_symbol_tables(self.context, array.context) 114 | self.context = ast.create_context( 115 | ast=ast.Node(moa_operation, None, (), (left_context.ast, right_context.ast)), 116 | symbol_table=new_symbol_table) 117 | 118 | elif isinstance(left, (int, float, str)): 119 | self.context = ast.Node(moa_operation, None, (), ( 120 | self.context.ast, 121 | self._create_array_from_int_float_string(array))) 122 | else: 123 | raise TypeError(f'not known how to handle outer product with type {type(array)}') 124 | return self 125 | 126 | def inner(self, left_operation, right_operation, array): 127 | if left_operation not in self.OPPERATION_MAP: 128 | raise ValueError(f'left operation {operation} not one of allowed operations {self.OPPERATION_MAP.keys()}') 129 | 130 | if right_operation not in self.OPPERATION_MAP: 131 | raise ValueError(f'right operation {operation} not one of allowed operations {self.OPPERATION_MAP.keys()}') 132 | 133 | moa_operation = (ast.NodeSymbol.DOT, self.OPPERATION_MAP[left_operation], self.OPPERATION_MAP[right_operation]) 134 | 135 | if isinstance(array, self.__class__): 136 | new_symbol_table, left_context, right_context = ast.join_symbol_tables(self.context, array.context) 137 | self.context = ast.create_context( 138 | ast=ast.Node(moa_operation, None, (), (left_context.ast, right_context.ast)), 139 | symbol_table=new_symbol_table) 140 | else: 141 | raise TypeError(f'not known how to handle outer product with type {type(array)}') 142 | return self 143 | 144 | def reduce(self, operation): 145 | if operation not in self.OPPERATION_MAP: 146 | raise ValueError(f'operation {operation} not one of allowed operations {self.OPPERATION_MAP.keys()}') 147 | 148 | moa_operation = (ast.NodeSymbol.REDUCE, self.OPPERATION_MAP[operation]) 149 | 150 | self.context = ast.create_context( 151 | ast=ast.Node(moa_operation, None, (), (self.context.ast,)), 152 | symbol_table=self.context.symbol_table) 153 | return self 154 | 155 | def _rbinary_opperation(self, operation, left): 156 | if isinstance(left, self.__class__): 157 | new_symbol_table, left_context, right_context = ast.join_symbol_tables(left.context, self.context) 158 | self.context = ast.create_context( 159 | ast=ast.Node((operation,), None, (), (left_context.ast, right_context.ast)), 160 | symbol_table=new_symbol_table) 161 | elif isinstance(left, (int, float, str)): 162 | self.context = ast.create_context( 163 | ast=ast.Node((operation,), None, (), (self._create_array_from_int_float_string(left), self.context.ast)), 164 | symbol_table=self.context.symbol_table) 165 | else: 166 | raise TypeError(f'not known how to handle binary operation with type {type(left)}') 167 | return self 168 | 169 | def _binary_opperation(self, operation, right): 170 | if isinstance(right, self.__class__): 171 | new_symbol_table, left_context, right_context = ast.join_symbol_tables(self.context, right.context) 172 | self.context = ast.create_context( 173 | ast=ast.Node((operation,), None, (), (left_context.ast, right_context.ast)), 174 | symbol_table=new_symbol_table) 175 | elif isinstance(right, (int, float, str)): 176 | self.context = ast.create_context( 177 | ast=ast.Node((operation,), None, (), (self.context.ast, self._create_array_from_int_float_string(right))), 178 | symbol_table=self.context.symbol_table) 179 | else: 180 | raise TypeError(f'not known how to handle binary operation with type {type(right)}') 181 | return self 182 | 183 | def __add__(self, other): 184 | return self._binary_opperation(ast.NodeSymbol.PLUS, other) 185 | 186 | def __radd__(self, other): 187 | return self._rbinary_opperation(ast.NodeSymbol.PLUS, other) 188 | 189 | def __sub__(self, other): 190 | return self._binary_opperation(ast.NodeSymbol.MINUS, other) 191 | 192 | def __rsub__(self, other): 193 | return self._rbinary_opperation(ast.NodeSymbol.MINUS, other) 194 | 195 | def __mul__(self, other): 196 | return self._binary_opperation(ast.NodeSymbol.TIMES, other) 197 | 198 | def __rmul__(self, other): 199 | return self._rbinary_opperation(ast.NodeSymbol.TIMES, other) 200 | 201 | def __truediv__(self, other): 202 | return self._binary_opperation(ast.NodeSymbol.DIVIDE, other) 203 | 204 | def __rtruediv__(self, other): 205 | return self._rbinary_opperation(ast.NodeSymbol.DIVIDE, other) 206 | 207 | def compile(self, backend='python', **kwargs): 208 | return compiler.compiler(self.context, backend=backend, **kwargs) 209 | 210 | def _shape(self): 211 | return calculate_shapes(self.context) 212 | 213 | def _dnf(self): 214 | return reduce_to_dnf(self._shape()) 215 | 216 | def _onf(self): 217 | return reduce_to_onf(self._dnf()) 218 | 219 | def analysis(self): 220 | shape_context = calculate_shapes(self.context) 221 | dnf_context = reduce_to_dnf(shape_context) 222 | 223 | return { 224 | 'unoptimized_flops': metric_flops(shape_context), 225 | 'optimized_flops': metric_flops(dnf_context) 226 | } 227 | 228 | def visualize(self, stage=None, as_text=False): 229 | if stage not in {None, 'shape', 'dnf', 'onf'}: 230 | raise ValueError('stage must be "shape", "dnf", or "onf"') 231 | 232 | if stage is None: 233 | context = self.context 234 | else: 235 | context = getattr(self, f'_{stage}')() 236 | 237 | if as_text: 238 | print_ast(context) 239 | else: 240 | return visualize_ast(context) 241 | 242 | def _repr_svg_(self): 243 | try: 244 | dot = self.visualize() 245 | return dot.pipe(format='svg').decode(dot._encoding) 246 | except ImportError as e: 247 | return None 248 | -------------------------------------------------------------------------------- /moa/frontend/moa.py: -------------------------------------------------------------------------------- 1 | """Parse Matematics of Arrays (MOA) Expression to internal ast representation 2 | 3 | """ 4 | 5 | import sly 6 | from sly.lex import LexError 7 | from sly.yacc import YaccError 8 | 9 | from .. import ast 10 | 11 | 12 | class MOALexer(sly.Lexer): 13 | tokens = { 14 | PLUS, MINUS, TIMES, DIVIDE, 15 | PSI, TAKE, DROP, CAT, 16 | IOTA, DIM, TAU, SHAPE, RAV, TRANSPOSE, 17 | LANGLEBRACKET, RANGLEBRACKET, 18 | LPAREN, RPAREN, 19 | CARROT, 20 | INTEGER, IDENTIFIER 21 | } 22 | 23 | ignore = ' \t' 24 | 25 | @_(r'\n+') 26 | def newline(self, t): 27 | self.lineno += len(t.value) 28 | pass 29 | 30 | def comment(self, t): 31 | pass # skip comments 32 | 33 | @_(r'[+-]?\d+') 34 | def INTEGER(self, t): 35 | t.value = int(t.value) 36 | return t 37 | 38 | IDENTIFIER = r'[a-zA-Z][a-zA-Z0-9_]*' 39 | 40 | ## containers 41 | LPAREN = r'\(' 42 | RPAREN = r'\)' 43 | LANGLEBRACKET = r'<' 44 | RANGLEBRACKET = r'>' 45 | CARROT = r'\^' 46 | 47 | ## unary operators 48 | IDENTIFIER['iota'] = IOTA 49 | IDENTIFIER['dim'] = DIM 50 | IDENTIFIER['shp'] = SHAPE 51 | IDENTIFIER['tau'] = TAU 52 | IDENTIFIER['rav'] = RAV 53 | IDENTIFIER['tran'] = TRANSPOSE 54 | 55 | ## binary operators 56 | PLUS = r'\+' 57 | MINUS = r'\-' 58 | TIMES = r'\*' 59 | DIVIDE = r'/' 60 | IDENTIFIER['psi'] = PSI 61 | IDENTIFIER['take'] = TAKE 62 | IDENTIFIER['drop'] = DROP 63 | IDENTIFIER['cat'] = CAT 64 | 65 | def error(self, t): 66 | raise ValueError(f"Illegal character '{t.value[0]}' no valid token can be formed from '{t.value}' on line {self.lineno}") 67 | 68 | 69 | class MOAParser(sly.Parser): 70 | tokens = MOALexer.tokens 71 | 72 | precedence = ( 73 | ('right', 'UNARYOP'), 74 | ('left', 'BINARYOP'), 75 | ('left', 'PLUS', 'MINUS'), 76 | ('left', 'TIMES', 'DIVIDE'), 77 | ) 78 | 79 | @_('LPAREN expr RPAREN') 80 | def expr(self, p): 81 | return p.expr 82 | 83 | @_('unary_operation expr %prec UNARYOP') 84 | def expr(self, p): 85 | return ast.Node((p.unary_operation,), None, (), (p.expr,)) 86 | 87 | @_('IOTA', 88 | 'DIM', 89 | 'TAU', 90 | 'SHAPE', 91 | 'RAV', 92 | 'TRANSPOSE') 93 | def unary_operation(self, p): 94 | unary_map = { 95 | 'tran': ast.NodeSymbol.TRANSPOSE, 96 | 'iota': ast.NodeSymbol.IOTA, 97 | 'dim': ast.NodeSymbol.DIM, 98 | 'tau': ast.NodeSymbol.TAU, 99 | 'shp': ast.NodeSymbol.SHAPE, 100 | 'rav': ast.NodeSymbol.RAV, 101 | } 102 | return unary_map[p[0].lower()] 103 | 104 | @_('expr binary_operation expr %prec BINARYOP') 105 | def expr(self, p): 106 | return ast.Node((p.binary_operation,), None, (), (p.expr0, p.expr1)) 107 | 108 | @_('PLUS', 109 | 'MINUS', 110 | 'TIMES', 111 | 'DIVIDE', 112 | 'PSI', 113 | 'TAKE', 114 | 'DROP', 115 | 'CAT', 116 | 'TRANSPOSE') 117 | def binary_operation(self, p): 118 | binary_map = { 119 | '+': ast.NodeSymbol.PLUS, 120 | '-': ast.NodeSymbol.MINUS, 121 | '*': ast.NodeSymbol.TIMES, 122 | '/': ast.NodeSymbol.DIVIDE, 123 | 'psi': ast.NodeSymbol.PSI, 124 | 'take': ast.NodeSymbol.TAKE, 125 | 'drop': ast.NodeSymbol.DROP, 126 | 'cat': ast.NodeSymbol.CAT, 127 | 'tran': ast.NodeSymbol.TRANSPOSEV, 128 | } 129 | return binary_map[p[0].lower()] 130 | 131 | @_('array') 132 | def expr(self, p): 133 | return p.array 134 | 135 | @_('IDENTIFIER CARROT LANGLEBRACKET vector_list RANGLEBRACKET') 136 | def array(self, p): 137 | self.context = ast.add_symbol(self.context, p.IDENTIFIER, ast.NodeSymbol.ARRAY, tuple(p.vector_list), None, None) 138 | return ast.Node((ast.NodeSymbol.ARRAY,), None, (p.IDENTIFIER,), ()) 139 | 140 | @_('IDENTIFIER') 141 | def array(self, p): 142 | self.context = ast.add_symbol(self.context, p.IDENTIFIER, ast.NodeSymbol.ARRAY, None, None, None) 143 | return ast.Node((ast.NodeSymbol.ARRAY,), None, (p.IDENTIFIER,), ()) 144 | 145 | @_('LANGLEBRACKET vector_list RANGLEBRACKET') 146 | def array(self, p): 147 | unique_array_name = ast.generate_unique_array_name(self.context) 148 | self.context = ast.add_symbol(self.context, unique_array_name, ast.NodeSymbol.ARRAY, (len(p.vector_list),), None, tuple(p.vector_list)) 149 | return ast.Node((ast.NodeSymbol.ARRAY,), None, (unique_array_name,), ()) 150 | 151 | @_('INTEGER vector_list') 152 | def vector_list(self, p): 153 | return (p.INTEGER,) + p.vector_list 154 | 155 | @_('IDENTIFIER vector_list') 156 | def vector_list(self, p): 157 | self.context = ast.add_symbol(self.context, p.IDENTIFIER, ast.NodeSymbol.ARRAY, (), None, None) 158 | return (ast.Node((ast.NodeSymbol.ARRAY,), (), (p.IDENTIFIER,), ()),) + p.vector_list 159 | 160 | @_('empty') 161 | def vector_list(self, p): 162 | return tuple() 163 | 164 | @_('') 165 | def empty(self, p): 166 | pass 167 | 168 | def error(self, p): 169 | if p: 170 | raise YaccError(f'Syntax error at line {p.lineno}, token={p.type}, value={p.value}\n') 171 | else: 172 | raise YaccError('Parse error in input. EOF\n') 173 | 174 | def parse(self, text): 175 | self.context = ast.create_context() 176 | 177 | lexer = MOALexer() 178 | tokens = lexer.tokenize(text) 179 | tree = super().parse(tokens) 180 | 181 | return ast.create_context(ast=tree, symbol_table=self.context.symbol_table) 182 | -------------------------------------------------------------------------------- /moa/frontend/numpy.py: -------------------------------------------------------------------------------- 1 | """Numpy frontend 2 | 3 | """ 4 | -------------------------------------------------------------------------------- /moa/shape.py: -------------------------------------------------------------------------------- 1 | from . import ast 2 | from .exception import MOAException 3 | 4 | 5 | class MOAShapeError(MOAException): 6 | pass 7 | 8 | 9 | # dimension 10 | def dimension(context, selection=()): 11 | context = ast.select_node(context, selection) 12 | 13 | if ast.is_array(context): 14 | return len(ast.select_array_node_symbol(context).shape) 15 | elif context.ast.shape is not None: 16 | return len(context.ast.shape) 17 | raise MOAShapeError(f'cannot determine dimension from node {node.node_type} with shape {node.shape}') 18 | 19 | 20 | def is_scalar(context, selection=()): 21 | context = ast.select_node(context, selection) 22 | return ast.is_array(context) and dimension(context) == 0 23 | 24 | 25 | def is_vector(context, selection=()): 26 | context = ast.select_node(context, selection) 27 | return ast.is_array(context) and dimension(context) == 1 28 | 29 | 30 | # compare tuples 31 | def compare_tuples(comparison, context, left_tuple, right_tuple, message): 32 | comparison_map = { 33 | ast.NodeSymbol.EQUAL: lambda e1, e2: e1 == e2, 34 | ast.NodeSymbol.NOTEQUAL: lambda e1, e2: e1 != e2, 35 | ast.NodeSymbol.LESSTHAN: lambda e1, e2: e1 < e2, 36 | ast.NodeSymbol.LESSTHANEQUAL: lambda e1, e2: e1 <= e2, 37 | ast.NodeSymbol.GREATERTHAN: lambda e1, e2: e1 > e2, 38 | ast.NodeSymbol.GREATERTHANEQUAL: lambda e1, e2: e1 >= e2, 39 | } 40 | 41 | conditions = () 42 | shape = () 43 | for i, (left_element, right_element) in enumerate(zip(left_tuple, right_tuple)): 44 | element_message = message + f' requires shapes to match elements #{i} left {left_element} != right {right_element}' 45 | 46 | if ast.is_symbolic_element(left_element) and ast.is_symbolic_element(right_element): # both are symbolic 47 | conditions = conditions + (ast.Node((comparison,), (), (), (left_element, right_element)),) 48 | shape = shape + (left_element,) 49 | elif ast.is_symbolic_element(left_element): # only left is symbolic 50 | array_name = ast.generate_unique_array_name(context) 51 | context = ast.add_symbol(context, array_name, ast.NodeSymbol.ARRAY, (), None, (right_element,)) 52 | conditions = conditions + (ast.Node((comparison,), (), (), (left_element, ast.Node((ast.NodeSymbol.ARRAY,), (), (array_name,), ()))),) 53 | shape = shape + (right_element,) 54 | elif ast.is_symbolic_element(right_element): # only right is symbolic 55 | array_name = ast.generate_unique_array_name(context) 56 | context = ast.add_symbol(context, array_name, ast.NodeSymbol.ARRAY, (), None, (left_element,)) 57 | conditions = conditions + (ast.Node((comparison,), (), (), (ast.Node((ast.NodeSymbol.ARRAY,), (), (array_name,), ()), right_element)),) 58 | shape = shape + (left_element,) 59 | else: # neither symbolic 60 | if not comparison_map[comparison](left_element, right_element): 61 | raise MOAShapeError(element_message) 62 | shape = shape + (left_element,) 63 | return context, conditions, shape 64 | 65 | 66 | def apply_node_conditions(context, conditions): 67 | if conditions: 68 | condition_node = conditions[0] 69 | for condition in conditions[1:]: 70 | condition_node = ast.Node((ast.NodeSymbol.AND,), (), (), (condition, condition_node)) 71 | 72 | context = ast.create_context( 73 | ast=ast.Node((ast.NodeSymbol.CONDITION,), context.ast.shape, (), (condition_node, context.ast)), 74 | symbol_table=context.symbol_table) 75 | return context 76 | 77 | 78 | # shape calculation 79 | def calculate_shapes(context): 80 | """Postorder traversal to calculate node shapes 81 | 82 | """ 83 | return ast.node_traversal(context, _shape_replacement, traversal='postorder') 84 | 85 | 86 | def _shape_replacement(context): 87 | shape_map = { 88 | (ast.NodeSymbol.ARRAY,): _shape_array, 89 | (ast.NodeSymbol.TRANSPOSE,): _shape_transpose, 90 | (ast.NodeSymbol.TRANSPOSEV,): _shape_transpose_vector, 91 | (ast.NodeSymbol.ASSIGN,): _shape_assign, 92 | (ast.NodeSymbol.SHAPE,): _shape_shape, 93 | (ast.NodeSymbol.PSI,): _shape_psi, 94 | (ast.NodeSymbol.PLUS,): _shape_plus_minus_divide_times, 95 | (ast.NodeSymbol.MINUS,): _shape_plus_minus_divide_times, 96 | (ast.NodeSymbol.TIMES,): _shape_plus_minus_divide_times, 97 | (ast.NodeSymbol.DIVIDE,): _shape_plus_minus_divide_times, 98 | (ast.NodeSymbol.DOT, ast.NodeSymbol.PLUS): _shape_outer_plus_minus_divide_times, 99 | (ast.NodeSymbol.DOT, ast.NodeSymbol.MINUS): _shape_outer_plus_minus_divide_times, 100 | (ast.NodeSymbol.DOT, ast.NodeSymbol.TIMES): _shape_outer_plus_minus_divide_times, 101 | (ast.NodeSymbol.DOT, ast.NodeSymbol.DIVIDE): _shape_outer_plus_minus_divide_times, 102 | (ast.NodeSymbol.REDUCE, ast.NodeSymbol.PLUS): _shape_reduce_plus_minus_divide_times, 103 | (ast.NodeSymbol.REDUCE, ast.NodeSymbol.MINUS): _shape_reduce_plus_minus_divide_times, 104 | (ast.NodeSymbol.REDUCE, ast.NodeSymbol.TIMES): _shape_reduce_plus_minus_divide_times, 105 | (ast.NodeSymbol.REDUCE, ast.NodeSymbol.DIVIDE): _shape_reduce_plus_minus_divide_times, 106 | (ast.NodeSymbol.DOT, ast.NodeSymbol.PLUS , ast.NodeSymbol.PLUS): _shape_inner_plus_minus_divide_times, 107 | (ast.NodeSymbol.DOT, ast.NodeSymbol.MINUS , ast.NodeSymbol.PLUS): _shape_inner_plus_minus_divide_times, 108 | (ast.NodeSymbol.DOT, ast.NodeSymbol.TIMES , ast.NodeSymbol.PLUS): _shape_inner_plus_minus_divide_times, 109 | (ast.NodeSymbol.DOT, ast.NodeSymbol.DIVIDE, ast.NodeSymbol.PLUS): _shape_inner_plus_minus_divide_times, 110 | (ast.NodeSymbol.DOT, ast.NodeSymbol.PLUS , ast.NodeSymbol.MINUS): _shape_inner_plus_minus_divide_times, 111 | (ast.NodeSymbol.DOT, ast.NodeSymbol.MINUS , ast.NodeSymbol.MINUS): _shape_inner_plus_minus_divide_times, 112 | (ast.NodeSymbol.DOT, ast.NodeSymbol.TIMES , ast.NodeSymbol.MINUS): _shape_inner_plus_minus_divide_times, 113 | (ast.NodeSymbol.DOT, ast.NodeSymbol.DIVIDE, ast.NodeSymbol.MINUS): _shape_inner_plus_minus_divide_times, 114 | (ast.NodeSymbol.DOT, ast.NodeSymbol.PLUS , ast.NodeSymbol.TIMES): _shape_inner_plus_minus_divide_times, 115 | (ast.NodeSymbol.DOT, ast.NodeSymbol.MINUS , ast.NodeSymbol.TIMES): _shape_inner_plus_minus_divide_times, 116 | (ast.NodeSymbol.DOT, ast.NodeSymbol.TIMES , ast.NodeSymbol.TIMES): _shape_inner_plus_minus_divide_times, 117 | (ast.NodeSymbol.DOT, ast.NodeSymbol.DIVIDE, ast.NodeSymbol.TIMES): _shape_inner_plus_minus_divide_times, 118 | (ast.NodeSymbol.DOT, ast.NodeSymbol.PLUS , ast.NodeSymbol.DIVIDE): _shape_inner_plus_minus_divide_times, 119 | (ast.NodeSymbol.DOT, ast.NodeSymbol.MINUS , ast.NodeSymbol.DIVIDE): _shape_inner_plus_minus_divide_times, 120 | (ast.NodeSymbol.DOT, ast.NodeSymbol.TIMES , ast.NodeSymbol.DIVIDE): _shape_inner_plus_minus_divide_times, 121 | (ast.NodeSymbol.DOT, ast.NodeSymbol.DIVIDE, ast.NodeSymbol.DIVIDE): _shape_inner_plus_minus_divide_times, 122 | } 123 | 124 | conditions = () 125 | for i in range(ast.num_node_children(context)): 126 | node = ast.select_node(context, (i,)).ast 127 | if node.symbol == (ast.NodeSymbol.CONDITION,): 128 | conditions = conditions + (node.child[0],) 129 | context = ast.replace_node(context, node.child[1], (i,)) 130 | 131 | context = shape_map[context.ast.symbol](context) 132 | 133 | if context.ast.symbol == (ast.NodeSymbol.CONDITION,): 134 | conditions = conditions + (context.ast.child[0],) 135 | context = ast.select_node(context, (1,)) 136 | 137 | context = apply_node_conditions(context, conditions) 138 | return context 139 | 140 | 141 | # Array Operations 142 | def _shape_array(context): 143 | shape = ast.select_array_node_symbol(context).shape 144 | return ast.replace_node_shape(context, shape) 145 | 146 | 147 | # Unary Operations 148 | def _shape_transpose(context): 149 | shape = ast.select_node_shape(context, (0,))[::-1] 150 | return ast.replace_node_shape(context, shape) 151 | 152 | 153 | def _shape_transpose_vector(context): 154 | if not is_vector(context, (0,)): 155 | raise MOAShapeError('TRANSPOSE VECTOR requires left node to be vector') 156 | 157 | left_node_symbol = ast.select_array_node_symbol(context, (0,)) 158 | if ast.has_symbolic_elements(left_node_symbol.shape) or ast.has_symbolic_elements(left_node_symbol.value): 159 | raise MOAShapeError('TRANSPOSE VECTOR not implemented for left node to be vector with symbolic components') 160 | 161 | if ast.select_node_shape(context, (0,))[0] != dimension(context, (1,)): 162 | raise MOAShapeError('TRANSPOSE VECTOR requires left node vector to have total number of elements equal to dimension of right node') 163 | 164 | if len(set(left_node_symbol.value)) != len(left_node_symbol.value): 165 | raise MOAShapeError('TRANSPOSE VECTOR requires left node vector to have unique elements') 166 | 167 | # sort two lists according to one list 168 | shape = tuple(s for _, s in sorted(zip(left_node_symbol.value, ast.select_node_shape(context, (1,))), key=lambda pair: pair[0])) 169 | return ast.replace_node_shape(context, shape) 170 | 171 | 172 | def _shape_assign(context): 173 | if dimension(context, (0,)) != dimension(context, (1,)): 174 | raise MOAShapeError('ASSIGN requires that the dimension of the left and right nodes to be same') 175 | 176 | context, conditions, shape = compare_tuples(ast.NodeSymbol.EQUAL, context, 177 | ast.select_node_shape(context, (0,)), 178 | ast.select_node_shape(context, (1,)), 'ASSIGN') 179 | 180 | context = ast.replace_node_shape(context, shape) 181 | return apply_node_conditions(context, conditions) 182 | 183 | 184 | def _shape_shape(context): 185 | shape = (dimension(ast.select_node(context, (0,))),) 186 | return ast.replace_node_shape(context, shape, ()) 187 | 188 | 189 | # Binary Operations 190 | def _shape_psi(context): 191 | if not is_vector(context, (0,)): 192 | raise MOAShapeError('PSI requires left node to be vector') 193 | 194 | left_node_symbol = ast.select_array_node_symbol(context, (0,)) 195 | if ast.has_symbolic_elements(left_node_symbol.shape): 196 | raise MOAShapeError('PSI not implemented for left node to be vector with symbolic shape') 197 | 198 | drop_dimensions = left_node_symbol.shape[0] 199 | if drop_dimensions > dimension(context, (1,)): 200 | raise MOAShapeError('PSI requires that vector length be no greater than dimension of right node') 201 | 202 | context, conditions, shape = compare_tuples(ast.NodeSymbol.LESSTHANEQUAL, context, 203 | left_node_symbol.value, 204 | ast.select_node_shape(context, (1,)), 'PSI') 205 | 206 | shape = ast.select_node_shape(context, (1,))[drop_dimensions:] 207 | context = ast.replace_node_shape(context, shape) 208 | return apply_node_conditions(context, conditions) 209 | 210 | 211 | def _shape_reduce_plus_minus_divide_times(context): 212 | if dimension(context, (0,)) == 0: 213 | return ast.select_node(context, (0,)) 214 | shape = ast.select_node_shape(context, (0,))[1:] 215 | return ast.replace_node_shape(context, shape) 216 | 217 | 218 | def _shape_outer_plus_minus_divide_times(context): 219 | shape = ast.select_node_shape(context, (0,)) + ast.select_node_shape(context, (1,)) 220 | return ast.replace_node_shape(context, shape) 221 | 222 | 223 | def _shape_inner_plus_minus_divide_times(context): 224 | left_shape = ast.select_node_shape(context, (0,)) 225 | right_shape = ast.select_node_shape(context, (1,)) 226 | 227 | context, conditions, shape = compare_tuples(ast.NodeSymbol.EQUAL, context, 228 | left_shape[-1:], right_shape[:1], 'INNER PRODUCT') 229 | shape = left_shape[:-1] + right_shape[1:] 230 | context = ast.replace_node_shape(context, shape) 231 | return apply_node_conditions(context, conditions) 232 | 233 | 234 | def _shape_plus_minus_divide_times(context): 235 | conditions = () 236 | if is_scalar(context, (0,)): # scalar extension 237 | shape = ast.select_node_shape(context, (1,)) 238 | elif is_scalar(context, (1,)): # scalar extension 239 | shape = ast.select_node_shape(context, (0,)) 240 | else: # shapes must match 241 | if dimension(context, (0,)) != dimension(context, (1,)): 242 | raise MOAShapeError('(+,-,/,*) requires dimension to match or single argument to be scalar') 243 | 244 | context, conditions, shape = compare_tuples(ast.NodeSymbol.EQUAL, context, 245 | ast.select_node_shape(context, (0,)), 246 | ast.select_node_shape(context, (1,)), '(+-*/)') 247 | 248 | context = ast.replace_node_shape(context, shape) 249 | return apply_node_conditions(context, conditions) 250 | -------------------------------------------------------------------------------- /moa/testing.py: -------------------------------------------------------------------------------- 1 | """Utilities to help with testing moa 2 | 3 | """ 4 | import copy 5 | 6 | from . import ast, visualize 7 | 8 | 9 | def assert_transformation(tree, symbol_table, expected_tree, expected_symbol_table, operation, debug=False): 10 | context = ast.create_context(ast=tree, symbol_table=symbol_table) 11 | expected_context = ast.create_context(ast=expected_tree, symbol_table=expected_symbol_table) 12 | 13 | if debug: 14 | visualize.print_ast(context) 15 | visualize.print_ast(expected_context) 16 | assert_context_transformation(context, expected_context, operation) 17 | 18 | 19 | def assert_context_transformation(context, expected_context, operation): 20 | context_copy = copy.deepcopy(context) 21 | 22 | new_context = operation(context) 23 | 24 | assert_context_equal(context, context_copy) 25 | assert_context_equal(new_context, expected_context) 26 | 27 | 28 | def assert_context_equal(left_context, right_context): 29 | assert_ast_equal(left_context.ast, right_context.ast) 30 | assert_symbol_table_equal(left_context.symbol_table, right_context.symbol_table) 31 | 32 | 33 | def assert_ast_equal(left_ast, right_ast, index=()): 34 | if left_ast.symbol != right_ast.symbol: 35 | raise ValueError(f'symbol {left_ast.symbol} != {right_ast.symbol} at node path {index}') 36 | 37 | if left_ast.shape != right_ast.shape: 38 | raise ValueError(f'shape {left_ast.shape} != {right_ast.shape} at node path {index}') 39 | 40 | if left_ast.attrib != right_ast.attrib: 41 | raise ValueError(f'attrib {left_ast.attrib} != {right_ast.attrib} at node path {index}') 42 | 43 | if len(left_ast.child) != len(right_ast.child): 44 | raise ValueError(f'left and right node have differing number of children {len(left_ast.child)} != {len(right_ast.child)} at node path {index}') 45 | 46 | for i, (left_child, right_child) in enumerate(zip(left_ast.child, right_ast.child)): 47 | assert_ast_equal(left_child, right_child, index + (i,)) 48 | 49 | 50 | def assert_symbol_table_equal(left_symbol_table, right_symbol_table): 51 | if left_symbol_table.keys() != right_symbol_table.keys(): 52 | raise ValueError(f'left symbol table is missing keys {left_symbol_table.keys() - right_symbol_table.keys()} and right is missing keys {right_symbol_table.keys() - left_symbol_table.keys()}') 53 | 54 | for key in left_symbol_table: 55 | for attr in {'symbol', 'shape', 'type', 'value'}: 56 | left_value = getattr(left_symbol_table[key], attr) 57 | right_value = getattr(right_symbol_table[key], attr) 58 | if left_value != right_value: 59 | raise ValueError(f'left and right symbol nodes at "{key}.{attr}" do not match {left_value} != {right_value}') 60 | -------------------------------------------------------------------------------- /moa/visualize.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | try: 4 | import graphviz 5 | except ImportError as e: 6 | graphviz = None 7 | 8 | from . import ast, backend 9 | 10 | _NODE_LABEL_MAP = { 11 | #Symbols 12 | (ast.NodeSymbol.ARRAY,): "Array", 13 | # Control 14 | (ast.NodeSymbol.FUNCTION,): "function", 15 | (ast.NodeSymbol.CONDITION,): "condition", 16 | (ast.NodeSymbol.LOOP,): "loop", 17 | (ast.NodeSymbol.INITIALIZE,): 'initialize', 18 | (ast.NodeSymbol.ERROR,): 'error', 19 | (ast.NodeSymbol.BLOCK,): 'block', 20 | # Unary 21 | (ast.NodeSymbol.IOTA,): "iota(ι)", 22 | (ast.NodeSymbol.DIM,): "dim(δ)", 23 | (ast.NodeSymbol.TAU,): "tau(τ)", 24 | (ast.NodeSymbol.SHAPE,): "shape(ρ)", 25 | (ast.NodeSymbol.RAV,): "rav", 26 | (ast.NodeSymbol.TRANSPOSE,): "transpose(Ø)", 27 | (ast.NodeSymbol.TRANSPOSEV,): "transpose(Ø)", 28 | (ast.NodeSymbol.REDUCE, ast.NodeSymbol.PLUS): 'reduce (+)', 29 | (ast.NodeSymbol.REDUCE, ast.NodeSymbol.MINUS): 'reduce (-)', 30 | (ast.NodeSymbol.REDUCE, ast.NodeSymbol.TIMES): 'reduce (*)', 31 | (ast.NodeSymbol.REDUCE, ast.NodeSymbol.DIVIDE): 'reduce (/)', 32 | # Binary 33 | (ast.NodeSymbol.ASSIGN,): 'assign', 34 | (ast.NodeSymbol.PLUS,): "+", 35 | (ast.NodeSymbol.MINUS,): "-", 36 | (ast.NodeSymbol.TIMES,): "*", 37 | (ast.NodeSymbol.DIVIDE,): "/", 38 | (ast.NodeSymbol.PSI,): "psi(Ψ)", 39 | (ast.NodeSymbol.DIM,): "dim(δ)", 40 | (ast.NodeSymbol.TAU,): "tau(τ)", 41 | (ast.NodeSymbol.TAKE,): "take(▵)", 42 | (ast.NodeSymbol.DROP,): "drop(▿)", 43 | (ast.NodeSymbol.CAT,): "cat(++)", 44 | (ast.NodeSymbol.DOT, ast.NodeSymbol.PLUS): 'outer (+)', 45 | (ast.NodeSymbol.DOT, ast.NodeSymbol.MINUS): 'outer (-)', 46 | (ast.NodeSymbol.DOT, ast.NodeSymbol.TIMES): 'outer (*)', 47 | (ast.NodeSymbol.DOT, ast.NodeSymbol.DIVIDE): 'outer (/)', 48 | # messy representation 49 | (ast.NodeSymbol.DOT, ast.NodeSymbol.PLUS , ast.NodeSymbol.PLUS): 'inner (+,+)', 50 | (ast.NodeSymbol.DOT, ast.NodeSymbol.MINUS , ast.NodeSymbol.PLUS): 'inner (-,+)', 51 | (ast.NodeSymbol.DOT, ast.NodeSymbol.TIMES , ast.NodeSymbol.PLUS): 'inner (*,+)', 52 | (ast.NodeSymbol.DOT, ast.NodeSymbol.DIVIDE, ast.NodeSymbol.PLUS): 'inner (/,+)', 53 | (ast.NodeSymbol.DOT, ast.NodeSymbol.PLUS , ast.NodeSymbol.MINUS): 'inner (+,-)', 54 | (ast.NodeSymbol.DOT, ast.NodeSymbol.MINUS , ast.NodeSymbol.MINUS): 'inner (-,-)', 55 | (ast.NodeSymbol.DOT, ast.NodeSymbol.TIMES , ast.NodeSymbol.MINUS): 'inner (*,-)', 56 | (ast.NodeSymbol.DOT, ast.NodeSymbol.DIVIDE, ast.NodeSymbol.MINUS): 'inner (/,-)', 57 | (ast.NodeSymbol.DOT, ast.NodeSymbol.PLUS , ast.NodeSymbol.TIMES): 'inner (+,*)', 58 | (ast.NodeSymbol.DOT, ast.NodeSymbol.MINUS , ast.NodeSymbol.TIMES): 'inner (-,*)', 59 | (ast.NodeSymbol.DOT, ast.NodeSymbol.TIMES , ast.NodeSymbol.TIMES): 'inner (*,*)', 60 | (ast.NodeSymbol.DOT, ast.NodeSymbol.DIVIDE, ast.NodeSymbol.TIMES): 'inner (/,*)', 61 | (ast.NodeSymbol.DOT, ast.NodeSymbol.PLUS , ast.NodeSymbol.DIVIDE): 'inner (+,/)', 62 | (ast.NodeSymbol.DOT, ast.NodeSymbol.MINUS , ast.NodeSymbol.DIVIDE): 'inner (-,/)', 63 | (ast.NodeSymbol.DOT, ast.NodeSymbol.TIMES , ast.NodeSymbol.DIVIDE): 'inner (*,/)', 64 | (ast.NodeSymbol.DOT, ast.NodeSymbol.DIVIDE, ast.NodeSymbol.DIVIDE): 'inner (/,/)', 65 | } 66 | 67 | 68 | def stringify_elements(context, elements): 69 | strings = [] 70 | for element in elements: 71 | if ast.is_symbolic_element(element): 72 | element_context = ast.create_context(ast=element, symbol_table=context.symbol_table) 73 | strings.append(backend.generate_python_source(element_context, materialize_scalars=True)) 74 | else: 75 | strings.append(str(element)) 76 | return strings 77 | 78 | 79 | def symbolic_tuple_string(context, shape, start='(', end=')'): 80 | return start + " ".join(stringify_elements(context, shape)) + end 81 | 82 | 83 | def escape_dot_string(string): 84 | return string.replace('<', '<').replace('>', '>') 85 | 86 | 87 | def _node_label(context): 88 | node_label = { 89 | 'name': _NODE_LABEL_MAP[context.ast.symbol], 90 | } 91 | # name 92 | if ast.is_array(context): 93 | node_label['name'] += f' {context.ast.attrib[0]}' 94 | 95 | # shape 96 | if ast.is_array(context): 97 | shape = ast.select_array_node_symbol(context).shape 98 | if shape is not None: 99 | node_label['shape'] = symbolic_tuple_string(context, shape, start='<', end='>') 100 | elif context.ast.shape is not None: 101 | node_label['shape'] = symbolic_tuple_string(context, context.ast.shape, start='<', end='>') 102 | 103 | # value 104 | if ast.is_array(context): 105 | value = ast.select_array_node_symbol(context).value 106 | if value is not None: 107 | node_label['value'] = symbolic_tuple_string(context, value, start='(', end=')') 108 | elif context.ast.symbol == (ast.NodeSymbol.ERROR,): 109 | message = context.ast.attrib[0] 110 | if message is not None: 111 | node_label['value'] = message 112 | elif context.ast.symbol in {(ast.NodeSymbol.LOOP,), (ast.NodeSymbol.INITIALIZE,)}: 113 | symbol_node = context.ast.attrib[0] 114 | if symbol_node is not None: 115 | node_label['value'] = symbol_node 116 | elif context.ast.symbol == (ast.NodeSymbol.CONDITION,): 117 | condition_context = ast.select_node(context, (0,)) 118 | node_label['value'] = backend.generate_python_source(condition_context, materialize_scalars=True) 119 | elif context.ast.symbol == (ast.NodeSymbol.FUNCTION,): 120 | arguments, result = context.ast.attrib[0], context.ast.attrib[1] 121 | if arguments is not None and result is not None: 122 | node_label['value'] = symbolic_tuple_string(context, arguments, start='(', end=')') + ' -> ' + result 123 | elif context.ast.symbol[0] == ast.NodeSymbol.REDUCE: 124 | if context.ast.attrib: 125 | node_label['value'] = context.ast.attrib[0] 126 | 127 | return node_label 128 | 129 | 130 | def print_ast(context, vector_value=True): 131 | def _print_node_label(context): 132 | node_label = _node_label(context) 133 | label = '{name}' 134 | if 'shape' in node_label: 135 | label += ': {shape}' 136 | if 'value' in node_label: 137 | label += ' {value}' 138 | return label.format(**node_label) 139 | 140 | def _print_node(context, prefix=""): 141 | if ast.num_node_children(context) == 0: 142 | return 143 | 144 | # no need to traverse condition node since converted to python source 145 | if context.ast.symbol == (ast.NodeSymbol.CONDITION,): 146 | child_context = ast.select_node(context, (1,)) 147 | print(prefix + "└──", _print_node_label(child_context)) 148 | _print_node(child_context, prefix + " ") 149 | return 150 | 151 | for i in range(ast.num_node_children(context) - 1): 152 | child_context = ast.select_node(context, (i,)) 153 | print(prefix + "├──", _print_node_label(child_context)) 154 | _print_node(child_context, prefix + "│ ") 155 | 156 | child_context = ast.select_node(context, (-1,)) 157 | print(prefix + "└──", _print_node_label(child_context)) 158 | _print_node(child_context, prefix + " ") 159 | 160 | print(_print_node_label(context)) 161 | _print_node(context) 162 | 163 | 164 | def visualize_ast(context, comment='MOA AST', with_attrs=True, vector_value=True): 165 | if graphviz is None: 166 | raise ImportError('The graphviz package is required to draw expressions') 167 | 168 | dot = graphviz.Digraph(comment=comment) 169 | counter = itertools.count() 170 | default_node_attr = dict(color='black', fillcolor='white', fontcolor='black') 171 | 172 | def _visualize_node_label(dot, context): 173 | unique_id = str(next(counter)) 174 | 175 | node_label = _node_label(context) 176 | for key, value in node_label.items(): 177 | node_label[key] = escape_dot_string(value) 178 | 179 | labels = [] 180 | if ast.is_array(context): 181 | shape = 'box' 182 | else: # operation 183 | shape = 'ellipse' 184 | 185 | if len(node_label) > 1: 186 | labels.append('{}'.format(node_label['name'])) 187 | if 'shape' in node_label: 188 | labels.append('\n{}'.format(node_label['shape'])) 189 | if 'value' in node_label: 190 | labels.append('\n{}'.format(node_label['value'])) 191 | 192 | node_description = f'''< 193 | 194 | {''.join(labels)} 195 |
>''' 196 | else: 197 | node_description = node_label['name'] 198 | 199 | dot.node(unique_id, label=node_description, shape=shape) 200 | return unique_id 201 | 202 | def _visualize_node(dot, context): 203 | node_id = _visualize_node_label(dot, context) 204 | 205 | # no need to traverse condition node since converted to python source 206 | if context.ast.symbol == (ast.NodeSymbol.CONDITION,): 207 | child_context = ast.select_node(context, (1,)) 208 | child_node_id = _visualize_node(dot, child_context) 209 | dot.edge(node_id, child_node_id) 210 | return node_id 211 | 212 | for i in range(ast.num_node_children(context)): 213 | child_context = ast.select_node(context, (i,)) 214 | child_node_id = _visualize_node(dot, child_context) 215 | dot.edge(node_id, child_node_id) 216 | 217 | return node_id 218 | 219 | _visualize_node(dot, context) 220 | return dot 221 | -------------------------------------------------------------------------------- /notebooks/4-benchmarks.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# TOC\n", 8 | "\n", 9 | " - [Introduction](0-introduction.ipynb)\n", 10 | " - [Simple Example](1-simple-example.ipynb)\n", 11 | " - [Simple Example Symbolic](2-simple-example-symblic.ipynb)\n", 12 | " - [Pythonic MOA Array Interface](3-lazy-arrays.ipynb)\n", 13 | " - [Benchmarks](4-benchmarks.ipynb)\n", 14 | " \n", 15 | "# Benchmarks\n", 16 | "\n", 17 | "Isn't performance the main reason we care about MOA?" 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": 1, 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "from moa.frontend import LazyArray\n", 27 | "from moa.array import Array\n", 28 | "\n", 29 | "n = 1000\n", 30 | "m = 1000" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": 2, 36 | "metadata": {}, 37 | "outputs": [], 38 | "source": [ 39 | "# A + B\n", 40 | "expression = (LazyArray(name='A', shape=('n', 'm')) + LazyArray(name='B', shape=('n', 'm')))[0]" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": 3, 46 | "metadata": {}, 47 | "outputs": [ 48 | { 49 | "data": { 50 | "image/svg+xml": [ 51 | "\n", 52 | "\n", 54 | "\n", 56 | "\n", 57 | "\n", 59 | "\n", 60 | "%3\n", 61 | "\n", 62 | "\n", 63 | "\n", 64 | "0\n", 65 | "\n", 66 | "psi(Ψ)\n", 67 | "\n", 68 | "\n", 69 | "\n", 70 | "1\n", 71 | "\n", 72 | "\n", 73 | "Array _a4\n", 74 | "\n", 75 | "<1>\n", 76 | "\n", 77 | "(0)\n", 78 | "\n", 79 | "\n", 80 | "\n", 81 | "0->1\n", 82 | "\n", 83 | "\n", 84 | "\n", 85 | "\n", 86 | "\n", 87 | "2\n", 88 | "\n", 89 | "+\n", 90 | "\n", 91 | "\n", 92 | "\n", 93 | "0->2\n", 94 | "\n", 95 | "\n", 96 | "\n", 97 | "\n", 98 | "\n", 99 | "3\n", 100 | "\n", 101 | "\n", 102 | "Array A\n", 103 | "\n", 104 | "<n m>\n", 105 | "\n", 106 | "\n", 107 | "\n", 108 | "2->3\n", 109 | "\n", 110 | "\n", 111 | "\n", 112 | "\n", 113 | "\n", 114 | "4\n", 115 | "\n", 116 | "\n", 117 | "Array B\n", 118 | "\n", 119 | "<n m>\n", 120 | "\n", 121 | "\n", 122 | "\n", 123 | "2->4\n", 124 | "\n", 125 | "\n", 126 | "\n", 127 | "\n", 128 | "\n" 129 | ], 130 | "text/plain": [ 131 | "" 132 | ] 133 | }, 134 | "execution_count": 3, 135 | "metadata": {}, 136 | "output_type": "execute_result" 137 | } 138 | ], 139 | "source": [ 140 | "expression" 141 | ] 142 | }, 143 | { 144 | "cell_type": "code", 145 | "execution_count": 4, 146 | "metadata": {}, 147 | "outputs": [], 148 | "source": [ 149 | "exec(expression.compile(backend='python'))\n", 150 | "\n", 151 | "A = Array(shape=(n, m), value=tuple(range(n*m)))\n", 152 | "B = Array(shape=(n, m), value=tuple(range(n*m)))" 153 | ] 154 | }, 155 | { 156 | "cell_type": "markdown", 157 | "metadata": {}, 158 | "source": [ 159 | "## Naive Pure Python MOA" 160 | ] 161 | }, 162 | { 163 | "cell_type": "code", 164 | "execution_count": 12, 165 | "metadata": {}, 166 | "outputs": [ 167 | { 168 | "name": "stdout", 169 | "output_type": "stream", 170 | "text": [ 171 | "1.68 µs ± 2.7 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)\n" 172 | ] 173 | } 174 | ], 175 | "source": [ 176 | "%%timeit\n", 177 | "\n", 178 | "f(A=A, B=B)" 179 | ] 180 | }, 181 | { 182 | "cell_type": "markdown", 183 | "metadata": {}, 184 | "source": [ 185 | "## Python MOA + Numba\n", 186 | "\n", 187 | "really just @numba.jit + numpy.zeros" 188 | ] 189 | }, 190 | { 191 | "cell_type": "code", 192 | "execution_count": 6, 193 | "metadata": {}, 194 | "outputs": [], 195 | "source": [ 196 | "import numpy\n", 197 | "import numba\n", 198 | "\n", 199 | "exec(expression.compile(backend='python', use_numba=True))\n", 200 | "\n", 201 | "A = numpy.arange(n*m).reshape((n, m))\n", 202 | "B = numpy.arange(n*m).reshape((n, m))" 203 | ] 204 | }, 205 | { 206 | "cell_type": "code", 207 | "execution_count": 7, 208 | "metadata": {}, 209 | "outputs": [ 210 | { 211 | "name": "stdout", 212 | "output_type": "stream", 213 | "text": [ 214 | "1.69 µs ± 6.31 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)\n" 215 | ] 216 | } 217 | ], 218 | "source": [ 219 | "%%timeit\n", 220 | "\n", 221 | "f(A=A, B=B)" 222 | ] 223 | }, 224 | { 225 | "cell_type": "markdown", 226 | "metadata": {}, 227 | "source": [ 228 | "## Naive Numpy" 229 | ] 230 | }, 231 | { 232 | "cell_type": "code", 233 | "execution_count": 8, 234 | "metadata": {}, 235 | "outputs": [ 236 | { 237 | "name": "stdout", 238 | "output_type": "stream", 239 | "text": [ 240 | "1.93 ms ± 66.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)\n" 241 | ] 242 | } 243 | ], 244 | "source": [ 245 | "%%timeit\n", 246 | "\n", 247 | "# lazy\n", 248 | "(A + B)[0]" 249 | ] 250 | }, 251 | { 252 | "cell_type": "markdown", 253 | "metadata": {}, 254 | "source": [ 255 | "## Optimized Numpy" 256 | ] 257 | }, 258 | { 259 | "cell_type": "code", 260 | "execution_count": 9, 261 | "metadata": {}, 262 | "outputs": [ 263 | { 264 | "name": "stdout", 265 | "output_type": "stream", 266 | "text": [ 267 | "1.46 µs ± 0.985 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)\n" 268 | ] 269 | } 270 | ], 271 | "source": [ 272 | "%%timeit\n", 273 | "\n", 274 | "# hand optimized\n", 275 | "(A[0] + B[0])" 276 | ] 277 | }, 278 | { 279 | "cell_type": "markdown", 280 | "metadata": {}, 281 | "source": [ 282 | "## Compilers are bad at math" 283 | ] 284 | }, 285 | { 286 | "cell_type": "code", 287 | "execution_count": 10, 288 | "metadata": {}, 289 | "outputs": [], 290 | "source": [ 291 | "@numba.jit\n", 292 | "def g(A, B):\n", 293 | " return (A + B)[0]" 294 | ] 295 | }, 296 | { 297 | "cell_type": "code", 298 | "execution_count": 11, 299 | "metadata": {}, 300 | "outputs": [ 301 | { 302 | "name": "stdout", 303 | "output_type": "stream", 304 | "text": [ 305 | "1.9 ms ± 69.1 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)\n" 306 | ] 307 | } 308 | ], 309 | "source": [ 310 | "%%timeit\n", 311 | "\n", 312 | "g(A, B)" 313 | ] 314 | } 315 | ], 316 | "metadata": { 317 | "kernelspec": { 318 | "display_name": "Python 3", 319 | "language": "python", 320 | "name": "python3" 321 | }, 322 | "language_info": { 323 | "codemirror_mode": { 324 | "name": "ipython", 325 | "version": 3 326 | }, 327 | "file_extension": ".py", 328 | "mimetype": "text/x-python", 329 | "name": "python", 330 | "nbconvert_exporter": "python", 331 | "pygments_lexer": "ipython3", 332 | "version": "3.7.2" 333 | } 334 | }, 335 | "nbformat": 4, 336 | "nbformat_minor": 2 337 | } 338 | -------------------------------------------------------------------------------- /notebooks/5-reduce.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 41, 6 | "metadata": {}, 7 | "outputs": [ 8 | { 9 | "data": { 10 | "image/svg+xml": [ 11 | "\n", 12 | "\n", 14 | "\n", 16 | "\n", 17 | "\n", 19 | "\n", 20 | "%3\n", 21 | "\n", 22 | "\n", 23 | "\n", 24 | "0\n", 25 | "\n", 26 | "reduce (+)\n", 27 | "\n", 28 | "\n", 29 | "\n", 30 | "1\n", 31 | "\n", 32 | "reduce (+)\n", 33 | "\n", 34 | "\n", 35 | "\n", 36 | "0->1\n", 37 | "\n", 38 | "\n", 39 | "\n", 40 | "\n", 41 | "\n", 42 | "2\n", 43 | "\n", 44 | "\n", 45 | "Array A\n", 46 | "\n", 47 | "<1000 1000 10>\n", 48 | "\n", 49 | "\n", 50 | "\n", 51 | "1->2\n", 52 | "\n", 53 | "\n", 54 | "\n", 55 | "\n", 56 | "\n" 57 | ], 58 | "text/plain": [ 59 | "" 60 | ] 61 | }, 62 | "execution_count": 41, 63 | "metadata": {}, 64 | "output_type": "execute_result" 65 | } 66 | ], 67 | "source": [ 68 | "from moa.frontend import LazyArray\n", 69 | "\n", 70 | "_A = LazyArray(shape=(1000, 1000, 10), name='A')\n", 71 | "\n", 72 | "expression = _A.reduce('+').reduce('+')\n", 73 | "expression" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": 42, 79 | "metadata": {}, 80 | "outputs": [ 81 | { 82 | "name": "stdout", 83 | "output_type": "stream", 84 | "text": [ 85 | "\n", 86 | "\n", 87 | "@numba.jit\n", 88 | "def f(A):\n", 89 | " \n", 90 | " \n", 91 | " if (not (len(A.shape) == 3)):\n", 92 | " \n", 93 | " raise Exception('arguments have invalid dimension')\n", 94 | " \n", 95 | " if (not ((10 == A.shape[2]) and ((1000 == A.shape[1]) and (1000 == A.shape[0])))):\n", 96 | " \n", 97 | " raise Exception('arguments do not match declared shape')\n", 98 | " \n", 99 | " _a16 = numpy.zeros(())\n", 100 | " \n", 101 | " _a18 = numpy.zeros(())\n", 102 | " \n", 103 | " _a14 = numpy.zeros((10,))\n", 104 | " \n", 105 | " for _i1 in range(0, 10, 1):\n", 106 | " \n", 107 | " _a18 = 0\n", 108 | " \n", 109 | " for _i3 in range(0, 1000, 1):\n", 110 | " \n", 111 | " _a16 = 0\n", 112 | " \n", 113 | " for _i5 in range(0, 1000, 1):\n", 114 | " \n", 115 | " _a16 = (_a16 + A[(_i5, _i3, _i1)])\n", 116 | " \n", 117 | " _a18 = (_a18 + _a16)\n", 118 | " \n", 119 | " _a14[(_i1,)] = _a18\n", 120 | " return _a14\n" 121 | ] 122 | } 123 | ], 124 | "source": [ 125 | "# numba doesn't optimize code\n", 126 | "# this means that you must move expressions\n", 127 | "# out of loops manually! (this made this code x1000 faster)\n", 128 | "print(expression.compile(use_numba=True))" 129 | ] 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": 43, 134 | "metadata": {}, 135 | "outputs": [], 136 | "source": [ 137 | "import numba\n", 138 | "import numpy\n", 139 | "\n", 140 | "A = numpy.random.random((1000, 1000, 10))\n", 141 | "\n", 142 | "exec(expression.compile(use_numba=True))" 143 | ] 144 | }, 145 | { 146 | "cell_type": "code", 147 | "execution_count": 44, 148 | "metadata": {}, 149 | "outputs": [ 150 | { 151 | "name": "stdout", 152 | "output_type": "stream", 153 | "text": [ 154 | "81.6 ms ± 375 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" 155 | ] 156 | } 157 | ], 158 | "source": [ 159 | "%%timeit\n", 160 | "\n", 161 | "f(A=A)" 162 | ] 163 | }, 164 | { 165 | "cell_type": "code", 166 | "execution_count": 45, 167 | "metadata": {}, 168 | "outputs": [ 169 | { 170 | "data": { 171 | "text/plain": [ 172 | "array([499937.96283215, 499907.21268619, 500088.56343786, 500273.82673681,\n", 173 | " 500498.60659954, 500016.55835758, 499734.80332504, 500553.13379038,\n", 174 | " 499661.78225155, 500064.78881993])" 175 | ] 176 | }, 177 | "execution_count": 45, 178 | "metadata": {}, 179 | "output_type": "execute_result" 180 | } 181 | ], 182 | "source": [ 183 | "f(A=A)" 184 | ] 185 | }, 186 | { 187 | "cell_type": "code", 188 | "execution_count": 46, 189 | "metadata": {}, 190 | "outputs": [ 191 | { 192 | "name": "stdout", 193 | "output_type": "stream", 194 | "text": [ 195 | "5.49 ms ± 85.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" 196 | ] 197 | } 198 | ], 199 | "source": [ 200 | "%%timeit\n", 201 | "\n", 202 | "A.sum(axis=0).sum(axis=0)" 203 | ] 204 | }, 205 | { 206 | "cell_type": "code", 207 | "execution_count": 47, 208 | "metadata": {}, 209 | "outputs": [ 210 | { 211 | "data": { 212 | "text/plain": [ 213 | "array([499937.96283215, 499907.21268619, 500088.56343786, 500273.82673681,\n", 214 | " 500498.60659954, 500016.55835758, 499734.80332504, 500553.13379038,\n", 215 | " 499661.78225155, 500064.78881993])" 216 | ] 217 | }, 218 | "execution_count": 47, 219 | "metadata": {}, 220 | "output_type": "execute_result" 221 | } 222 | ], 223 | "source": [ 224 | "A.sum(axis=0).sum(axis=0)" 225 | ] 226 | }, 227 | { 228 | "cell_type": "code", 229 | "execution_count": null, 230 | "metadata": {}, 231 | "outputs": [], 232 | "source": [] 233 | } 234 | ], 235 | "metadata": { 236 | "kernelspec": { 237 | "display_name": "Python 3", 238 | "language": "python", 239 | "name": "python3" 240 | }, 241 | "language_info": { 242 | "codemirror_mode": { 243 | "name": "ipython", 244 | "version": 3 245 | }, 246 | "file_extension": ".py", 247 | "mimetype": "text/x-python", 248 | "name": "python", 249 | "nbconvert_exporter": "python", 250 | "pygments_lexer": "ipython3", 251 | "version": "3.7.2" 252 | } 253 | }, 254 | "nbformat": 4, 255 | "nbformat_minor": 2 256 | } 257 | -------------------------------------------------------------------------------- /release.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import { } }: 2 | 3 | # let pkgs = import (builtins.fetchTarball { 4 | # url = "https://github.com/costrouc/nixpkgs/archive/14775a074cfacc59482d1adf0446801d38c08216.tar.gz"; 5 | # sha256 = "152dflinv7a0nk267nc1i3ldvrx5fwxm7cf3igxc0qnd92n82phf"; 6 | # }) { }; 7 | 8 | let build = import ./build.nix { 9 | inherit pkgs; 10 | pythonPackages = pkgs.python3Packages; 11 | }; 12 | in { 13 | python-moa = build.package; 14 | python-moa-docs = build.docs; 15 | python-moa-sdist = build.sdist; 16 | python-moa-docker = build.docker; 17 | } 18 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from os import path 3 | 4 | here = path.abspath(path.dirname(__file__)) 5 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 6 | long_description = f.read() 7 | 8 | setup( 9 | name='python-moa', 10 | version='0.5.1', 11 | python_requires='>=3.6', 12 | description='Python Mathematics of Arrays (MOA)', 13 | long_description=long_description, 14 | long_description_content_type='text/markdown', 15 | url='https://github.com/Quansight-Labs/python-moa', 16 | keywords='tensor,compiler,moa', 17 | maintainer='Christopher Ostrouchov', 18 | maintainer_email='costrouchov@quansight.com', 19 | license='BSD 3-Clause License (Revised)', 20 | classifiers=[ 21 | 'Development Status :: 3 - Alpha', 22 | "License :: OSI Approved :: BSD License", 23 | 'Programming Language :: Python :: 3', 24 | 'Programming Language :: Python :: 3.6', 25 | 'Programming Language :: Python :: 3.7', 26 | 'Programming Language :: Python :: 3 :: Only', 27 | 'Intended Audience :: Developers', 28 | 'Intended Audience :: Science/Research', 29 | ], 30 | packages=find_packages(exclude=['docs', 'tests', 'notebooks', 'benchmarks']), 31 | install_requires=['astunparse'], 32 | extras_require={ 33 | 'moa': ['sly'], 34 | 'viz': ['graphviz'], 35 | 'test': ['sly', 'pytest', 'pytest-cov'], 36 | 'docs': ['sphinx', 'sphinxcontrib-tikz'], 37 | 'benchmark': ['numpy', 'numba'] 38 | }, 39 | project_urls={ 40 | 'Bug Reports': 'https://github.com/Quansight-Labs/python-moa/issues', 41 | 'Source': 'https://github.com/Quansight-Labs/python-moa/sampleproject/', 42 | 'Documentation': 'https://python-moa.readthedocs.io' 43 | }, 44 | ) 45 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quansight-Labs/python-moa/1f02519425ab0215896a5fb9be00631c20d34895/tests/__init__.py -------------------------------------------------------------------------------- /tests/backend/test_python.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | import astunparse 4 | import pytest 5 | 6 | from moa import ast, backend 7 | 8 | 9 | @pytest.mark.parametrize('symbol_table, tree, expected_source', [ 10 | # (+-/*) 11 | (ast.NodeSymbol.PLUS, '+'), 12 | (ast.NodeSymbol.MINUS, '-'), 13 | (ast.NodeSymbol.TIMES, '*'), 14 | (ast.NodeSymbol.DIVIDE, '/'), 15 | # ==, !=, <, >, <=, >= 16 | (ast.NodeSymbol.EQUAL, '=='), 17 | (ast.NodeSymbol.NOTEQUAL, '!='), 18 | (ast.NodeSymbol.LESSTHAN, '<'), 19 | (ast.NodeSymbol.LESSTHANEQUAL, '<='), 20 | (ast.NodeSymbol.LESSTHAN, '>'), 21 | (ast.NodeSymbol.LESSTHANEQUAL, '>='), 22 | ]) 23 | def test_python_backend_unit(operation, operation_string): 24 | symbol_table = { 25 | 'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (), None, (4,)), 26 | 'B': ast.SymbolNode(ast.NodeSymbol.ARRAY, (), None, (3,)) 27 | } 28 | tree = ast.Node((operation,), (3, 4), (), ( 29 | ast.Node((ast.NodeSymbol.ARRAY,), (3, 4), ('A',), ()), 30 | ast.Node((ast.NodeSymbol.ARRAY,), (3, 4), ('B',), ()))) 31 | 32 | context = ast.create_context(ast=tree, symbol_table=symbol_table) 33 | expected_source = f'(A {operation_string} B)' 34 | assert expected_source == backend.generate_python_source(context) 35 | 36 | 37 | @pytest.mark.parametrize('symbol_table, tree, expected_source', [ 38 | ({ 39 | 'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (), None, (4,)), 40 | 'B': ast.SymbolNode(ast.NodeSymbol.ARRAY, (), None, (3,)) 41 | }, 42 | ast.Node((ast.NodeSymbol.GREATERTHANEQUAL,), (), (), ( 43 | ast.Node((ast.NodeSymbol.ARRAY,), (), ('A',), ()), 44 | ast.Node((ast.NodeSymbol.PLUS,), (), (), ( 45 | ast.Node((ast.NodeSymbol.ARRAY,), (), ('A',), ()), 46 | ast.Node((ast.NodeSymbol.ARRAY,), (), ('B',), ()))))), 47 | '(A >= (A + B))'), 48 | ({ 49 | 'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (), None, (4,)), 50 | 'B': ast.SymbolNode(ast.NodeSymbol.ARRAY, (), None, (3,)) 51 | }, 52 | ast.Node((ast.NodeSymbol.AND,), (), (), ( 53 | ast.Node((ast.NodeSymbol.LESSTHAN,), (), (), ( 54 | ast.Node((ast.NodeSymbol.ARRAY,), (), ('A',), ()), 55 | ast.Node((ast.NodeSymbol.ARRAY,), (), ('B',), ()))), 56 | ast.Node((ast.NodeSymbol.EQUAL,), (), (), ( 57 | ast.Node((ast.NodeSymbol.ARRAY,), (), ('B',), ()), 58 | ast.Node((ast.NodeSymbol.ARRAY,), (), ('A',), ()))))), 59 | '((A < B) and (B == A))'), 60 | ({ 61 | 'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (), None, (4,)), 62 | 'B': ast.SymbolNode(ast.NodeSymbol.ARRAY, (), None, (3,)) 63 | }, 64 | ast.Node((ast.NodeSymbol.OR,), (), (), ( 65 | ast.Node((ast.NodeSymbol.LESSTHAN,), (), (), ( 66 | ast.Node((ast.NodeSymbol.ARRAY,), (), ('A',), ()), 67 | ast.Node((ast.NodeSymbol.ARRAY,), (), ('B',), ()))), 68 | ast.Node((ast.NodeSymbol.EQUAL,), (), (), ( 69 | ast.Node((ast.NodeSymbol.ARRAY,), (), ('B',), ()), 70 | ast.Node((ast.NodeSymbol.ARRAY,), (), ('A',), ()))))), 71 | '((A < B) or (B == A))'), 72 | ]) 73 | def test_python_backend_unit(symbol_table, tree, expected_source): 74 | context = ast.create_context(ast=tree, symbol_table=symbol_table) 75 | assert expected_source == backend.generate_python_source(context) 76 | 77 | 78 | def test_python_backend_materialize_scalar(): 79 | symbol_table = { 80 | 'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (), None, (4,)), 81 | 'B': ast.SymbolNode(ast.NodeSymbol.ARRAY, (), None, (3,)) 82 | } 83 | 84 | tree = ast.Node((ast.NodeSymbol.OR,), (), (), ( 85 | ast.Node((ast.NodeSymbol.LESSTHAN,), (), (), ( 86 | ast.Node((ast.NodeSymbol.ARRAY,), (), ('A',), ()), 87 | ast.Node((ast.NodeSymbol.ARRAY,), (), ('B',), ()))), 88 | ast.Node((ast.NodeSymbol.EQUAL,), (), (), ( 89 | ast.Node((ast.NodeSymbol.ARRAY,), (), ('B',), ()), 90 | ast.Node((ast.NodeSymbol.ARRAY,), (), ('A',), ()))))) 91 | 92 | context = ast.create_context(ast=tree, symbol_table=symbol_table) 93 | assert backend.generate_python_source(context, materialize_scalars=True) == '((4 < 3) or (3 == 4))' 94 | 95 | 96 | @pytest.mark.parametrize('symbol_table, tree, expected_source', [ 97 | # Lenore Simple Example #1 06/01/2018 98 | ({'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (3, 4), None, None), 99 | 'B': ast.SymbolNode(ast.NodeSymbol.ARRAY, (3, 4), None, None), 100 | '_i3': ast.SymbolNode(ast.NodeSymbol.INDEX, (), None, (0, 3, 1)), 101 | '_a4': ast.SymbolNode(ast.NodeSymbol.ARRAY, (2,), None, (ast.Node((ast.NodeSymbol.ARRAY,), (), ('_i3',), ()), 0))}, 102 | ast.Node((ast.NodeSymbol.PLUS,), (3,), (), ( 103 | ast.Node((ast.NodeSymbol.PSI,), (3,), (), ( 104 | ast.Node((ast.NodeSymbol.ARRAY,), (2,), ('_a4',), ()), 105 | ast.Node((ast.NodeSymbol.ARRAY,), (3, 4), ('A',), ()))), 106 | ast.Node((ast.NodeSymbol.PSI,), (3,), (), ( 107 | ast.Node((ast.NodeSymbol.ARRAY,), (2,), ('_a4',), ()), 108 | ast.Node((ast.NodeSymbol.ARRAY,), (3, 4), ('B',), ()))))), 109 | "(A[(_i3, 0)] + B[(_i3, 0)])" 110 | ) 111 | ]) 112 | def test_python_backend_integration(symbol_table, tree, expected_source): 113 | context = ast.create_context(ast=tree, symbol_table=symbol_table) 114 | assert expected_source == backend.generate_python_source(context) 115 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quansight-Labs/python-moa/1f02519425ab0215896a5fb9be00631c20d34895/tests/conftest.py -------------------------------------------------------------------------------- /tests/frontend/test_array.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from moa.frontend import LazyArray 4 | from moa import ast, testing, visualize 5 | 6 | 7 | def test_array_single_array(): 8 | expression = LazyArray(name='A', shape=(2, 3)) 9 | node = ast.Node((ast.NodeSymbol.ARRAY,), None, ('A',), ()) 10 | symbol_table = {'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (2, 3), None, None)} 11 | context = ast.create_context(ast=node, symbol_table=symbol_table) 12 | 13 | testing.assert_context_equal(context, expression.context) 14 | 15 | 16 | def test_array_single_array_symbolic(): 17 | expression = LazyArray(name='A', shape=('n', 3)) 18 | node = ast.Node((ast.NodeSymbol.ARRAY,), None, ('A',), ()) 19 | symbol_table = { 20 | 'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (ast.Node((ast.NodeSymbol.ARRAY,), (), ('n',), ()), 3), None, None), 21 | 'n': ast.SymbolNode(ast.NodeSymbol.ARRAY, (), None, None), 22 | } 23 | context = ast.create_context(ast=node, symbol_table=symbol_table) 24 | 25 | testing.assert_context_equal(context, expression.context) 26 | 27 | 28 | @pytest.mark.parametrize("function, side, operation", [ 29 | (lambda: LazyArray(name='A', shape=(2, 3)) + 1, 'right', ast.NodeSymbol.PLUS), 30 | (lambda: 1 + LazyArray(name='A', shape=(2, 3)), 'left', ast.NodeSymbol.PLUS), 31 | (lambda: LazyArray(name='A', shape=(2, 3)) - 1, 'right', ast.NodeSymbol.MINUS), 32 | (lambda: 1 - LazyArray(name='A', shape=(2, 3)), 'left', ast.NodeSymbol.MINUS), 33 | (lambda: LazyArray(name='A', shape=(2, 3)) * 1, 'right', ast.NodeSymbol.TIMES), 34 | (lambda: 1 * LazyArray(name='A', shape=(2, 3)), 'left', ast.NodeSymbol.TIMES), 35 | (lambda: LazyArray(name='A', shape=(2, 3)) / 1, 'right', ast.NodeSymbol.DIVIDE), 36 | (lambda: 1 / LazyArray(name='A', shape=(2, 3)), 'left', ast.NodeSymbol.DIVIDE), 37 | ]) 38 | def test_array_single_array_binary_operation_cast(function, side, operation): 39 | expression = function() 40 | if side == 'right': 41 | tree = ast.Node((operation,), None, (), ( 42 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('A',), ()), 43 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('_a1',), ()))) 44 | else: 45 | tree = ast.Node((operation,), None, (), ( 46 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('_a1',), ()), 47 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('A',), ()))) 48 | symbol_table = { 49 | 'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (2, 3), None, None), 50 | '_a1': ast.SymbolNode(ast.NodeSymbol.ARRAY, (), None, (1,)) 51 | } 52 | context = ast.create_context(ast=tree, symbol_table=symbol_table) 53 | 54 | testing.assert_context_equal(context, expression.context) 55 | 56 | 57 | def test_array_addition(): 58 | expression = LazyArray(name='A', shape=(2, 3)) + LazyArray(name='B', shape=(2, 3)) 59 | tree = ast.Node((ast.NodeSymbol.PLUS,), None, (), ( 60 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('A',), ()), 61 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('B',), ()))) 62 | symbol_table = { 63 | 'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (2, 3), None, None), 64 | 'B': ast.SymbolNode(ast.NodeSymbol.ARRAY, (2, 3), None, None) 65 | } 66 | context = ast.create_context(ast=tree, symbol_table=symbol_table) 67 | 68 | testing.assert_context_equal(context, expression.context) 69 | 70 | 71 | def test_array_transpose_T(): 72 | expression = LazyArray(name='A', shape=(2, 3)).T 73 | node = ast.Node((ast.NodeSymbol.TRANSPOSE,), None, (), ( 74 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('A',), ()),)) 75 | symbol_table = {'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (2, 3), None, None)} 76 | context = ast.create_context(ast=node, symbol_table=symbol_table) 77 | 78 | testing.assert_context_equal(context, expression.context) 79 | 80 | 81 | def test_array_transpose_default(): 82 | expression = LazyArray(name='A', shape=(2, 3)).transpose() 83 | node = ast.Node((ast.NodeSymbol.TRANSPOSE,), None, (), ( 84 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('A',), ()),)) 85 | symbol_table = {'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (2, 3), None, None)} 86 | context = ast.create_context(ast=node, symbol_table=symbol_table) 87 | 88 | testing.assert_context_equal(context, expression.context) 89 | 90 | 91 | def test_array_transpose_with_vector(): 92 | expression = LazyArray(name='A', shape=(2, 3)).transpose([1, 0]) 93 | node = ast.Node((ast.NodeSymbol.TRANSPOSEV,), None, (), ( 94 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('_a1',), ()), 95 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('A',), ()),)) 96 | symbol_table = { 97 | 'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (2, 3), None, None), 98 | '_a1': ast.SymbolNode(ast.NodeSymbol.ARRAY, (2,), None, (1, 0)), 99 | } 100 | context = ast.create_context(ast=node, symbol_table=symbol_table) 101 | 102 | testing.assert_context_equal(context, expression.context) 103 | 104 | 105 | @pytest.mark.parametrize("symbol", [ 106 | '+', '-', '*', '/' 107 | ]) 108 | def test_array_outer_product(symbol): 109 | expression = LazyArray(name='A', shape=(2, 3)).outer(symbol, LazyArray(name='B', shape=(1, 2))) 110 | 111 | expected_tree = ast.Node((ast.NodeSymbol.DOT, LazyArray.OPPERATION_MAP[symbol]), None, (), ( 112 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('A',), ()), 113 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('B',), ()))) 114 | expected_symbol_table = { 115 | 'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (2, 3), None, None), 116 | 'B': ast.SymbolNode(ast.NodeSymbol.ARRAY, (1, 2), None, None), 117 | } 118 | expected_context = ast.create_context(ast=expected_tree, symbol_table=expected_symbol_table) 119 | 120 | testing.assert_context_equal(expected_context, expression.context) 121 | 122 | 123 | @pytest.mark.parametrize("left_symbol, right_symbol", [ 124 | ('+', '+'), 125 | ('-', '+'), 126 | ('*', '+'), 127 | ('/', '+'), 128 | ('+', '-'), 129 | ('-', '-'), 130 | ('*', '-'), 131 | ('/', '-'), 132 | ('+', '*'), 133 | ('-', '*'), 134 | ('*', '*'), 135 | ('/', '*'), 136 | ('+', '/'), 137 | ('-', '/'), 138 | ('*', '/'), 139 | ('/', '/'), 140 | ]) 141 | def test_array_inner_product(left_symbol, right_symbol): 142 | expression = LazyArray(name='A', shape=(2, 3)).inner(left_symbol, right_symbol, LazyArray(name='B', shape=(3, 4))) 143 | 144 | expected_tree = ast.Node((ast.NodeSymbol.DOT, LazyArray.OPPERATION_MAP[left_symbol], LazyArray.OPPERATION_MAP[right_symbol]), None, (), ( 145 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('A',), ()), 146 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('B',), ()))) 147 | expected_symbol_table = { 148 | 'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (2, 3), None, None), 149 | 'B': ast.SymbolNode(ast.NodeSymbol.ARRAY, (3, 4), None, None), 150 | } 151 | expected_context = ast.create_context(ast=expected_tree, symbol_table=expected_symbol_table) 152 | 153 | testing.assert_context_equal(expected_context, expression.context) 154 | 155 | 156 | @pytest.mark.parametrize("symbol", [ 157 | '+', '-', '*', '/' 158 | ]) 159 | def test_array_reduce(symbol): 160 | expression = LazyArray(name='A', shape=(2, 3)).reduce(symbol) 161 | 162 | expected_tree = ast.Node((ast.NodeSymbol.REDUCE, LazyArray.OPPERATION_MAP[symbol]), None, (), ( 163 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('A',), ()),)) 164 | expected_symbol_table = { 165 | 'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (2, 3), None, None), 166 | } 167 | expected_context = ast.create_context(ast=expected_tree, symbol_table=expected_symbol_table) 168 | 169 | testing.assert_context_equal(expected_context, expression.context) 170 | 171 | 172 | 173 | def test_array_index_int(): 174 | expression = LazyArray(name='A', shape=(2, 3))[0] 175 | node = ast.Node((ast.NodeSymbol.PSI,), None, (), ( 176 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('_a1',), ()), 177 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('A',), ()),)) 178 | symbol_table = { 179 | 'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (2, 3), None, None), 180 | '_a1': ast.SymbolNode(ast.NodeSymbol.ARRAY, (1,), None, (0,)), 181 | } 182 | context = ast.create_context(ast=node, symbol_table=symbol_table) 183 | 184 | testing.assert_context_equal(context, expression.context) 185 | 186 | 187 | def test_array_index_symbol(): 188 | expression = LazyArray(name='A', shape=(2, 3))['n'] 189 | node = ast.Node((ast.NodeSymbol.PSI,), None, (), ( 190 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('_a2',), ()), 191 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('A',), ()),)) 192 | symbol_table = { 193 | 'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (2, 3), None, None), 194 | 'n': ast.SymbolNode(ast.NodeSymbol.ARRAY, (), None, None), 195 | '_a2': ast.SymbolNode(ast.NodeSymbol.ARRAY, (1,), None, (ast.Node((ast.NodeSymbol.ARRAY,), (), ('n',), ()),)), 196 | } 197 | context = ast.create_context(ast=node, symbol_table=symbol_table) 198 | 199 | testing.assert_context_equal(context, expression.context) 200 | 201 | 202 | @pytest.mark.xfail 203 | def test_array_index_stride(): 204 | expression = LazyArray(name='A', shape=(2, 3))[1:2] 205 | tree = ast.Node((ast.NodeSymbol.PSI,), None, (), ( 206 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('_a2',), ()), 207 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('A',), ()))) 208 | symbol_table = { 209 | 'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (2, 3), None, None), 210 | 'n': ast.SymbolNode(ast.NodeSymbol.ARRAY, (), None, None), 211 | '_a2': ast.SymbolNode(ast.NodeSymbol.ARRAY, (1,), None, (Node(ast.NodeSymbol.ARRAY, (), 'n'),)), 212 | } 213 | context = ast.create_context(ast=tree, symbol_table=symbol_table) 214 | 215 | testing.assert_context_equal(context, expression.context) 216 | 217 | 218 | @pytest.mark.xfail 219 | def test_array_index_stride_reverse(): 220 | expression = LazyArray(name='A', shape=(2, 3))[1:2:-1] 221 | tree = ast.Node(ast.NodeSymbol.PSI, None, (), ( 222 | ast.Node(ast.NodeSymbol.ARRAY, None, ('_a2',), ()), 223 | ast.Node(ast.NodeSymbol.ARRAY, None, ('A',), ()))) 224 | symbol_table = { 225 | 'A': SymbolNode(ast.NodeSymbol.ARRAY, (2, 3), None, None), 226 | 'n': SymbolNode(ast.NodeSymbol.ARRAY, (), None, None), 227 | '_a2': SymbolNode(ast.NodeSymbol.ARRAY, (1,), None, (Node(ast.NodeSymbol.ARRAY, (), 'n'),)), 228 | } 229 | context = ast.create_context(ast=tree, symbol_table=symbol_table) 230 | 231 | testing.assert_context_equal(context, expression.context) 232 | 233 | 234 | def test_array_index_tuple(): 235 | expression = LazyArray(name='A', shape=(2, 3))[1, 0] 236 | node = ast.Node((ast.NodeSymbol.PSI,), None, (), ( 237 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('_a1',), ()), 238 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('A',), ()),)) 239 | symbol_table = { 240 | 'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (2, 3), None, None), 241 | '_a1': ast.SymbolNode(ast.NodeSymbol.ARRAY, (2,), None, (1, 0)), 242 | } 243 | context = ast.create_context(ast=node, symbol_table=symbol_table) 244 | 245 | testing.assert_context_equal(context, expression.context) 246 | -------------------------------------------------------------------------------- /tests/frontend/test_moa.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | sly = pytest.importorskip('sly') 4 | 5 | from moa.frontend.moa import MOALexer, MOAParser 6 | from moa.exception import MOAException 7 | from moa import testing 8 | from moa import ast 9 | 10 | 11 | @pytest.mark.parametrize("expression,result", [ 12 | ('1234', ('INTEGER',)), 13 | ('A', ('IDENTIFIER',)), 14 | ('asdf_asAVA', ('IDENTIFIER',)), 15 | ('+', ('PLUS',)), 16 | ('-', ('MINUS',)), 17 | ('*', ('TIMES',)), 18 | ('/', ('DIVIDE',)), 19 | ('psi', ('PSI',)), 20 | ('take', ('TAKE',)), 21 | ('drop', ('DROP',)), 22 | ('cat', ('CAT',)), 23 | ('iota', ('IOTA',)), 24 | ('dim', ('DIM',)), 25 | ('tau', ('TAU',)), 26 | ('shp', ('SHAPE',)), 27 | ('rav', ('RAV',)), 28 | ('tran', ('TRANSPOSE',)), 29 | ('(', ('LPAREN',)), 30 | (')', ('RPAREN',)), 31 | ('<', ('LANGLEBRACKET',)), 32 | ('>', ('RANGLEBRACKET',)), 33 | ('^', ('CARROT',)), 34 | ]) 35 | def test_lexer_single_token(expression, result): 36 | lexer = MOALexer() 37 | tokens = tuple(token.type for token in lexer.tokenize(expression)) 38 | assert tokens == result 39 | 40 | 41 | @pytest.mark.parametrize("expression,symbol_table,tree", [ 42 | # symbolic vector 43 | ('<1 b> psi A ^ <1 2 a>', 44 | {'a': ast.SymbolNode(ast.NodeSymbol.ARRAY, (), None, None), 45 | 'b': ast.SymbolNode(ast.NodeSymbol.ARRAY, (), None, None), 46 | '_a1': ast.SymbolNode(ast.NodeSymbol.ARRAY, (2,), None, (1, ast.Node((ast.NodeSymbol.ARRAY,), (), ('b',), ()))), 47 | 'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (1, 2, ast.Node((ast.NodeSymbol.ARRAY,), (), ('a',), ())), None, None)}, 48 | ast.Node((ast.NodeSymbol.PSI,), None, (), ( 49 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('_a1',), ()), 50 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('A',), ()),))), 51 | # binary transpose 52 | ('<3 1 2> tran A', 53 | {'_a0': ast.SymbolNode(ast.NodeSymbol.ARRAY, (3,), None, (3, 1, 2)), 54 | 'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, None, None, None)}, 55 | ast.Node((ast.NodeSymbol.TRANSPOSEV,), None, (), ( 56 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('_a0',), ()), 57 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('A',), ()),))), 58 | # indexing 59 | ('<1 2 3> psi A', 60 | {'_a0': ast.SymbolNode(ast.NodeSymbol.ARRAY, (3,), None, (1, 2, 3)), 61 | 'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, None, None, None)}, 62 | ast.Node((ast.NodeSymbol.PSI,), None, (), ( 63 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('_a0',), ()), 64 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('A',), ()),))), 65 | ('<1 2 3> cat A ^ <3 5 7 9>', 66 | {'_a0': ast.SymbolNode(ast.NodeSymbol.ARRAY, (3,), None, (1, 2, 3)), 67 | 'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (3, 5, 7, 9), None, None)}, 68 | ast.Node((ast.NodeSymbol.CAT,), None, (), ( 69 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('_a0',), ()), 70 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('A',), ()),))), 71 | ('<0> psi ( tran (A + B))', 72 | {'_a0': ast.SymbolNode(ast.NodeSymbol.ARRAY, (1,), None, (0,)), 73 | 'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, None, None, None), 74 | 'B': ast.SymbolNode(ast.NodeSymbol.ARRAY, None, None, None)}, 75 | ast.Node((ast.NodeSymbol.PSI,), None, (), ( 76 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('_a0',), ()), 77 | ast.Node((ast.NodeSymbol.TRANSPOSE,), None, (), ( 78 | ast.Node((ast.NodeSymbol.PLUS,), None, (), ( 79 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('A',), ()), 80 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('B',), ()),)),)),))), 81 | # Lenore Simple Example #1 06/01/2018 82 | ('<0> psi ( tran (A^<3 4> + B^<3 4>))', 83 | {'_a0': ast.SymbolNode(ast.NodeSymbol.ARRAY, (1,), None, (0,)), 84 | 'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (3, 4), None, None), 85 | 'B': ast.SymbolNode(ast.NodeSymbol.ARRAY, (3, 4), None, None)}, 86 | ast.Node((ast.NodeSymbol.PSI,), None, (), ( 87 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('_a0',), ()), 88 | ast.Node((ast.NodeSymbol.TRANSPOSE,), None, (), ( 89 | ast.Node((ast.NodeSymbol.PLUS,), None, (), ( 90 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('A',), ()), 91 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('B',), ()),)),)),))), 92 | ]) 93 | def test_parser_simple_expressions(expression, symbol_table, tree): 94 | parser = MOAParser() 95 | expected_context = ast.create_context(ast=tree, symbol_table=symbol_table) 96 | 97 | testing.assert_context_equal(parser.parse(expression), expected_context) 98 | 99 | 100 | def test_parser_shape_mismatch_declaration(): 101 | parser = MOAParser() 102 | expression = 'A ^ <2 3> + A ^ <2 3 4>' 103 | with pytest.raises(MOAException): 104 | parser.parse(expression) 105 | -------------------------------------------------------------------------------- /tests/test_analysis.py: -------------------------------------------------------------------------------- 1 | from moa import ast, analysis, shape, dnf, visualize 2 | 3 | 4 | def test_metric_flops(): 5 | tree = ast.Node((ast.NodeSymbol.PSI,), None, (), ( 6 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('_a1',), ()), 7 | ast.Node((ast.NodeSymbol.TRANSPOSE,), None, (), ( 8 | ast.Node((ast.NodeSymbol.PLUS,), None, (), ( 9 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('A',), ()), 10 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('B',), ()))),)))) 11 | 12 | symbol_table = { 13 | '_a1': ast.SymbolNode(ast.NodeSymbol.ARRAY, (1,), None, (0,)), 14 | 'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (10, 100), None, None), 15 | 'B': ast.SymbolNode(ast.NodeSymbol.ARRAY, (10, 100), None, None) 16 | } 17 | 18 | context = ast.create_context(ast=tree, symbol_table=symbol_table) 19 | context = shape.calculate_shapes(context) 20 | 21 | assert analysis.metric_flops(context) == 1000 22 | 23 | context = dnf.reduce_to_dnf(context) 24 | 25 | assert analysis.metric_flops(context) == 10 26 | -------------------------------------------------------------------------------- /tests/test_array.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from moa.array import Array 4 | 5 | 6 | def test_array_invalid_shape(): 7 | Array(shape=(2, 3, 2), value=(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)) 8 | 9 | with pytest.raises(ValueError): 10 | Array(shape=(1, 2, 3), value=(1, 2), fmt='row') 11 | 12 | 13 | def test_array_dimension(): 14 | a = Array(shape=(1, 2, 3), value=(1, 2, 3, 4, 5, 6), fmt='row') 15 | 16 | assert len(a.shape) == 3 17 | 18 | 19 | def test_array_get_index(): 20 | a = Array(shape=(1, 2, 3), value=(1, 2, 3, 4, 5, 6), fmt='row') 21 | assert a[0, 0, 0] == 1 22 | assert a[0, 0, 1] == 2 23 | assert a[0, 0, 2] == 3 24 | assert a[0, 1, 0] == 4 25 | assert a[0, 1, 2] == 6 26 | 27 | # partial index 28 | with pytest.raises(IndexError): 29 | a[1, 1] 30 | 31 | # out of bounds index 32 | with pytest.raises(IndexError): 33 | a[1, 1, 1] == 10 34 | 35 | 36 | def test_array_set_index(): 37 | a = Array(shape=(1, 2, 3), value=(1, 2, 3, 4, 5, 6), fmt='row') 38 | a[0, 1, 1] = 10 39 | a[0, 1, 1] == 10 40 | 41 | 42 | def test_array_scalar_opperations_invalid(): 43 | a = Array(shape=(1, 2), value=(1, 2)) 44 | 45 | with pytest.raises(TypeError): 46 | a + 1 47 | 48 | with pytest.raises(TypeError): 49 | 1 + a 50 | 51 | with pytest.raises(TypeError): 52 | a - 1 53 | 54 | with pytest.raises(TypeError): 55 | 1 - a 56 | 57 | with pytest.raises(TypeError): 58 | a * 1 59 | 60 | with pytest.raises(TypeError): 61 | 1 * a 62 | 63 | with pytest.raises(TypeError): 64 | a / 1 65 | 66 | with pytest.raises(TypeError): 67 | 1 / a 68 | 69 | 70 | def test_array_scalar_opperations_valid(): 71 | a = Array(shape=(), value=(3,)) 72 | 73 | assert a + 1 == 4 74 | assert 1 + a == 4 75 | assert a - 1 == 2 76 | assert 1 - a == -2 77 | assert a * 1 == 3 78 | assert 1 * a == 3 79 | assert a / 1 == 3 80 | assert abs(1 / a - 0.3333333333333) < 1e-6 81 | 82 | 83 | def test_array_scalar_comparison_invalid(): 84 | a = Array(shape=(1, 2), value=(1, 2)) 85 | 86 | with pytest.raises(TypeError): 87 | a > 1 88 | 89 | with pytest.raises(TypeError): 90 | 1 > a 91 | 92 | with pytest.raises(TypeError): 93 | a >= 1 94 | 95 | with pytest.raises(TypeError): 96 | 1 >= a 97 | 98 | with pytest.raises(TypeError): 99 | a < 1 100 | 101 | with pytest.raises(TypeError): 102 | 1 < a 103 | 104 | with pytest.raises(TypeError): 105 | a <= 1 106 | 107 | with pytest.raises(TypeError): 108 | 1 <= a 109 | 110 | with pytest.raises(TypeError): 111 | a == 1 112 | 113 | with pytest.raises(TypeError): 114 | 1 == a 115 | 116 | with pytest.raises(TypeError): 117 | a != 1 118 | 119 | with pytest.raises(TypeError): 120 | 1 != a 121 | 122 | 123 | def test_array_scalar_comparison_valid(): 124 | a = Array(shape=(), value=(3,)) 125 | 126 | assert (a > 1) == True 127 | assert (1 > a) == False 128 | assert (a >= 1) == True 129 | assert (1 >= a) == False 130 | assert (a < 1) == False 131 | assert (1 < a) == True 132 | assert (a <= 1) == False 133 | assert (1 <= a) == True 134 | assert (a == 1) == False 135 | assert (1 == a) == False 136 | assert (a != 1) == True 137 | assert (1 != a) == True 138 | -------------------------------------------------------------------------------- /tests/test_compiler.py: -------------------------------------------------------------------------------- 1 | """high level tests 2 | 3 | """ 4 | import pytest 5 | 6 | from moa.frontend import LazyArray 7 | from moa.compiler import compiler 8 | from moa.array import Array 9 | 10 | 11 | def test_lenore_example_1(): 12 | _A = LazyArray(name='A', shape=(2, 3)) 13 | _B = LazyArray(name='B', shape=(2, 3)) 14 | python_source = compiler((_A + _B).T[0].context) 15 | print(python_source) 16 | 17 | local_dict = {} 18 | exec(python_source, globals(), local_dict) 19 | 20 | A = Array((2, 3), (1, 2, 3, 4, 5, 6)) 21 | B = Array((2, 3), (7, 8, 9, 10, 11, 12)) 22 | C = local_dict['f'](A, B) 23 | assert C.shape == (2,) 24 | assert C.value == [8, 14] 25 | 26 | 27 | def test_lenore_example_1_symbols(): 28 | _A = LazyArray(name='A', shape=('n', 'm')) 29 | _B = LazyArray(name='B', shape=('l', 3)) 30 | python_source = compiler((_A + _B).T['i'].context) 31 | 32 | local_dict = {} 33 | exec(python_source, globals(), local_dict) 34 | 35 | A = Array((2, 3), (1, 2, 3, 4, 5, 6)) 36 | B = Array((2, 3), (7, 8, 9, 10, 11, 12)) 37 | i = Array((), (0)) 38 | C = local_dict['f'](A=A, B=B, i=i) 39 | assert C.shape == (2,) 40 | assert C.value == [8, 14] 41 | -------------------------------------------------------------------------------- /tests/test_integration.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from moa.frontend import LazyArray 4 | from moa.array import Array 5 | 6 | 7 | def test_array_reduction(): 8 | _A = LazyArray(name='A', shape=(3, 2)) 9 | 10 | expression = _A.reduce('+') 11 | 12 | local_dict = {} 13 | print(expression.compile()) 14 | exec(expression.compile(), globals(), local_dict) 15 | 16 | A = Array(shape=(3, 2), value=tuple(range(1, 7))) 17 | B = local_dict['f'](A=A) 18 | 19 | assert B.shape == (2,) 20 | assert B.value == [9, 12] 21 | 22 | 23 | @pytest.mark.xfail 24 | def test_array_complex_slice(): 25 | _A = LazyArray(name='A', shape=(3, 4, 5)) 26 | _B = LazyArray(name='B', shape=(3, 4, 5)) 27 | 28 | expression = _A[0, 0:2:-1] + _B[:, 1:3, :][1] 29 | 30 | local_dict = {} 31 | exec(expression.compile(), globals(), local_dict) 32 | 33 | A = Array(shape=(3, 4, 5), value=tuple(range(1, 60))) 34 | B = Array(shape=(3, 4, 5), value=tuple(range(61, 121))) 35 | B = local_dict['f'](A=A) 36 | 37 | assert B.shape == (3, 5) 38 | assert B.value == [3, 7, 11] 39 | 40 | 41 | def test_array_frontend_transpose_vector_outer_scalar_addition(): 42 | _A = LazyArray(name='A', shape=(3, 2)) 43 | _B = LazyArray(name='B', shape=(4,)) 44 | _C = LazyArray(name='C', shape=(3, 4)) 45 | 46 | expression = (((_A.T)[0] - 1).outer('*', _B) + _C + 'n').transpose([1, 0]) 47 | 48 | local_dict = {} 49 | exec(expression.compile(), globals(), local_dict) 50 | 51 | A = Array(shape=(3, 2), value=(1, 2, 3, 4, 5, 6)) 52 | B = Array(shape=(4,), value=(13, 14, 15, 16)) 53 | C = Array(shape=(3, 4), value=(17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28)) 54 | n = Array(shape=(), value=(4,)) 55 | 56 | D = local_dict['f'](A=A, B=B, C=C, n=n) 57 | 58 | assert D.shape == (4, 3) 59 | assert D.value == [21, 51, 81, 22, 54, 86, 23, 57, 91, 24, 60, 96] 60 | -------------------------------------------------------------------------------- /tests/test_visualize.py: -------------------------------------------------------------------------------- 1 | from moa.visualize import visualize_ast, print_ast 2 | from moa import ast 3 | 4 | 5 | def test_graphviz_visualization(): 6 | symbol_table = {'_a1': ast.SymbolNode(ast.NodeSymbol.ARRAY, (1,), None, (0,)), 7 | 'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (3, 4), None, None), 8 | 'B': ast.SymbolNode(ast.NodeSymbol.ARRAY, (3, 4), None, None)} 9 | tree = ast.Node((ast.NodeSymbol.PSI,), None, (), ( 10 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('_a1',), ()), 11 | ast.Node((ast.NodeSymbol.TRANSPOSE,), None, (), ( 12 | ast.Node((ast.NodeSymbol.PLUS,), None, (), ( 13 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('A',), ()), 14 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('B',), ()),)),)))) 15 | 16 | context = ast.create_context(ast=tree, symbol_table=symbol_table) 17 | visualize_ast(context) 18 | 19 | 20 | def test_print_visualization(): 21 | symbol_table = {'_a1': ast.SymbolNode(ast.NodeSymbol.ARRAY, (1,), None, (0,)), 22 | 'A': ast.SymbolNode(ast.NodeSymbol.ARRAY, (3, 4), None, None), 23 | 'B': ast.SymbolNode(ast.NodeSymbol.ARRAY, (3, 4), None, None)} 24 | tree = ast.Node((ast.NodeSymbol.PSI,), None, (), ( 25 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('_a1',), ()), 26 | ast.Node((ast.NodeSymbol.TRANSPOSE,), None, (), ( 27 | ast.Node((ast.NodeSymbol.PLUS,), None, (), ( 28 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('A',), ()), 29 | ast.Node((ast.NodeSymbol.ARRAY,), None, ('B',), ()),)),)))) 30 | 31 | context = ast.create_context(ast=tree, symbol_table=symbol_table) 32 | print_ast(context) 33 | --------------------------------------------------------------------------------