From 743ac682c4f8d18d1f87cccc0b0dd0ce1832355d Mon Sep 17 00:00:00 2001 From: Aditya Bhosale Date: Sat, 12 Feb 2022 21:43:31 -0600 Subject: [PATCH 01/16] Add files --- README.rst | 0 charm/__init__.py | 1 + charm/numpy/__init__.py | 0 charm/numpy/core.pxd | 35 ++++++++++++++++++++ charm/numpy/core.pyx | 49 +++++++++++++++++++++++++++ setup.py | 73 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 158 insertions(+) create mode 100644 README.rst create mode 100644 charm/__init__.py create mode 100644 charm/numpy/__init__.py create mode 100644 charm/numpy/core.pxd create mode 100644 charm/numpy/core.pyx create mode 100644 setup.py diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..e69de29 diff --git a/charm/__init__.py b/charm/__init__.py new file mode 100644 index 0000000..a1c1976 --- /dev/null +++ b/charm/__init__.py @@ -0,0 +1 @@ +__version__ = '0.1.dev' diff --git a/charm/numpy/__init__.py b/charm/numpy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/charm/numpy/core.pxd b/charm/numpy/core.pxd new file mode 100644 index 0000000..4990b27 --- /dev/null +++ b/charm/numpy/core.pxd @@ -0,0 +1,35 @@ +# cython: language_level=3, embedsignature=True +# distutils: language=c++ + +cimport numpy as np +import numpy as np + +from libc.stdlib cimport malloc, free + + +cdef extern from "charm.h": + void StartCharmExt(int argc, char **argv) + + +cdef extern from "" namespace "aum" nogil: + cdef cppclass matrix: + matrix() except + + matrix(int, int) except + + + matrix& operator=(const matrix&) + matrix& operator=(matrix) + + matrix operator+(const matrix&, const matrix&) + matrix operator+(matrix, const matrix&) + matrix operator+(const matrix&, matrix) + matrix operator+(matrix, matrix) + + +cdef class pymatrix: + cdef public np.ndarray shape + cdef matrix _data + + cdef inline matrix& get_data(self) nogil + cdef inline void set_data(self, matrix& mat) nogil + + cdef inline void _allocate(self, shape) diff --git a/charm/numpy/core.pyx b/charm/numpy/core.pyx new file mode 100644 index 0000000..e40571b --- /dev/null +++ b/charm/numpy/core.pyx @@ -0,0 +1,49 @@ +# cython: language_level=3, embedsignature=True +# distutils: language=c++ + +import sys + + +def initialize(): + args = [arg.encode() for arg in sys.argv] + cdef int num_args = len(args) + cdef int i = 0 + try: + argv = malloc(num_args * sizeof(char*)) + for i in range(num_args): + argv[i] = args[i] + StartCharmExt(num_args, argv) + finally: + free(argv) + + +cdef pymatrix wrap_matrix(matrix& mat): + cdef pymatrix pymat = pymatrix() + pymat.set_data(mat) + return pymat + + +cdef class pymatrix: + def __init__(self, shape=None): + if shape: + self.shape = shape + else: + self.shape = np.array([0, 0], dtype=np.int32) + + def __cinit__(self, shape=None): + if shape: + self._allocate(shape) + + def __add__(self, other): + cdef matrix result = self._data + other.get_data() + return wrap_matrix(result) + + cdef inline void _allocate(self, shape): + self._data = matrix(shape[0], shape[1]) + + cdef inline matrix& get_data(self) nogil: + return self._data + + cdef inline void set_data(self, matrix& mat) nogil: + self._data = mat + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..cd25964 --- /dev/null +++ b/setup.py @@ -0,0 +1,73 @@ +import sys +from setuptools import setup, find_packages + +try: + from Cython.Distutils import Extension + from Cython.Build import cythonize +except ImportError: + from distutils.core import Extension + + def cythonize(*args, **kw): + return args[0] + + +def get_version(): + import os + data = {} + fname = os.path.join('charm.numpy', '__init__.py') + exec(compile(open(fname).read(), fname, 'exec'), data) + return data.get('__version__') + + +install_requires = ['cython', 'numpy', 'charm4py'] +tests_require = ['pytest'] +if sys.version_info[0] < 3: + tests_require += ['mock>=1.0'] +docs_require = ['sphinx'] + +classes = ''' +Development Status :: 4 - Beta +Intended Audience :: Developers +Intended Audience :: Science/Research +License :: OSI Approved :: BSD License +Natural Language :: English +Operating System :: MacOS :: MacOS X +Operating System :: Microsoft :: Windows +Operating System :: POSIX +Operating System :: Unix +Programming Language :: Python +Programming Language :: Python :: 2.7 +Programming Language :: Python :: 3 +Topic :: Scientific/Engineering +Topic :: Software Development :: Libraries +Topic :: Utilities +''' +classifiers = [x.strip() for x in classes.splitlines() if x] + +ext_modules = [ + Extension( + name="charm.numpy.core", + sources=["charm/numpy/core.pyx"], + language="c++" + ), + ] + +setup( + name='charm.numpy', + version=get_version(), + author='Aditya Bhosale', + author_email='adityapb1546@gmail.com', + description='A python library for distributed array computations', + long_description=open('README.rst').read(), + license="BSD", + url='https://github.com/adityapb/charm.numpy', + classifiers=classifiers, + packages=find_packages(), + ext_modules=cythonize(ext_modules, language="c++"), + install_requires=install_requires, + extras_require={ + "docs": docs_require, + "tests": tests_require, + "dev": docs_require + tests_require, + }, +) From 9852a2e3d9c139c14932e19334b29df9f1891da6 Mon Sep 17 00:00:00 2001 From: Aditya Bhosale Date: Fri, 1 Apr 2022 14:46:32 -0500 Subject: [PATCH 02/16] Clean up --- charm/__init__.py | 1 - charm/numpy/__init__.py | 0 charm/numpy/core.pxd | 35 ----------- charm/numpy/core.pyx | 49 ---------------- pyproject/array.py | 116 ++++++++++++++++++++++++++++++++++++ pyproject/server.ci | 9 +++ pyproject/server.cpp | 127 ++++++++++++++++++++++++++++++++++++++++ setup.py | 28 ++------- 8 files changed, 256 insertions(+), 109 deletions(-) delete mode 100644 charm/__init__.py delete mode 100644 charm/numpy/__init__.py delete mode 100644 charm/numpy/core.pxd delete mode 100644 charm/numpy/core.pyx create mode 100644 pyproject/array.py create mode 100644 pyproject/server.ci create mode 100644 pyproject/server.cpp diff --git a/charm/__init__.py b/charm/__init__.py deleted file mode 100644 index a1c1976..0000000 --- a/charm/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = '0.1.dev' diff --git a/charm/numpy/__init__.py b/charm/numpy/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/charm/numpy/core.pxd b/charm/numpy/core.pxd deleted file mode 100644 index 4990b27..0000000 --- a/charm/numpy/core.pxd +++ /dev/null @@ -1,35 +0,0 @@ -# cython: language_level=3, embedsignature=True -# distutils: language=c++ - -cimport numpy as np -import numpy as np - -from libc.stdlib cimport malloc, free - - -cdef extern from "charm.h": - void StartCharmExt(int argc, char **argv) - - -cdef extern from "" namespace "aum" nogil: - cdef cppclass matrix: - matrix() except + - matrix(int, int) except + - - matrix& operator=(const matrix&) - matrix& operator=(matrix) - - matrix operator+(const matrix&, const matrix&) - matrix operator+(matrix, const matrix&) - matrix operator+(const matrix&, matrix) - matrix operator+(matrix, matrix) - - -cdef class pymatrix: - cdef public np.ndarray shape - cdef matrix _data - - cdef inline matrix& get_data(self) nogil - cdef inline void set_data(self, matrix& mat) nogil - - cdef inline void _allocate(self, shape) diff --git a/charm/numpy/core.pyx b/charm/numpy/core.pyx deleted file mode 100644 index e40571b..0000000 --- a/charm/numpy/core.pyx +++ /dev/null @@ -1,49 +0,0 @@ -# cython: language_level=3, embedsignature=True -# distutils: language=c++ - -import sys - - -def initialize(): - args = [arg.encode() for arg in sys.argv] - cdef int num_args = len(args) - cdef int i = 0 - try: - argv = malloc(num_args * sizeof(char*)) - for i in range(num_args): - argv[i] = args[i] - StartCharmExt(num_args, argv) - finally: - free(argv) - - -cdef pymatrix wrap_matrix(matrix& mat): - cdef pymatrix pymat = pymatrix() - pymat.set_data(mat) - return pymat - - -cdef class pymatrix: - def __init__(self, shape=None): - if shape: - self.shape = shape - else: - self.shape = np.array([0, 0], dtype=np.int32) - - def __cinit__(self, shape=None): - if shape: - self._allocate(shape) - - def __add__(self, other): - cdef matrix result = self._data + other.get_data() - return wrap_matrix(result) - - cdef inline void _allocate(self, shape): - self._data = matrix(shape[0], shape[1]) - - cdef inline matrix& get_data(self) nogil: - return self._data - - cdef inline void set_data(self, matrix& mat) nogil: - self._data = mat - diff --git a/pyproject/array.py b/pyproject/array.py new file mode 100644 index 0000000..5ada70c --- /dev/null +++ b/pyproject/array.py @@ -0,0 +1,116 @@ +import sys +import numpy as np +from pyccs import Server + +server = None + +def create_ndarray(name, ndim, shape, dtype): + return ndarray(ndim, shape, dtype, name) + +def send_command(handler, msg, reply_size=8): + server.send_request(handler, 0, msg) + return int.from_bytes(server.receive_response(reply_size), "little") + +def connect(server_ip, server_port): + global server + server = Server(server_ip, server_port) + server.connect() + +class ndarray: + def __init__(self, ndim, shape, dtype, name=None): + """ + This is the wrapper class for AUM array objects. + The argument 'name' should be None except when wrapping + an array that already exists on the AUM backend server + """ + if ndim > 2: + raise NotImplementedError("Arrays of dimensionality greater than" + "2 not supported yet") + self.dtype = dtype + self.ndim = ndim + self.itemsize = np.dtype(dtype).itemsize + self.shape = np.asarray(shape, dtype=np.int32) + self.size = np.prod(self.shape) + self._creation_handler = b'aum_creation' + self._operation_handler = b'aum_operation' + self._fetch_handler = b'aum_fetch' + self._exit_handler = b'aum_exit' + if name: + self.name = name + else: + cmd = self._get_creation_command() + self.name = send_command(self._creation_handler, cmd) + + def _get_creation_command(self): + """ + Generate array creation CCS command + """ + cmd = int(self.ndim).to_bytes(4, "little") + for s in self.shape: + cmd += int(s).to_bytes(8, "little") + return cmd + + def _get_fetch_command(self): + """ + Generate CCS command to fetch entire array data + """ + pass + + def _get_operation_command(self, operation, other): + """ + Generate CCS command for operation + """ + pass + + def __del__(self): + pass + + def __len__(self): + return self.shape[0] + + def __str__(self): + pass + + def __repr__(self): + pass + + def __add__(self, other): + if (self.shape != other.shape).any(): + raise RuntimeError("Shape mismatch %s and %s" % (self.shape, + other.shape)) + cmd = self._get_operation_command("+", other) + res = send_command(self._operation_handler, cmd) + return create_ndarray(res, self.ndim, self.shape, self.dtype) + + def __radd__(self, other): + return self + other + + def __sub__(self, other): + if (self.shape != other.shape).any(): + raise RuntimeError("Shape mismatch %s and %s" % (self.shape, + other.shape)) + cmd = self._get_operation_command("-", other) + res = send_command(self._operation_handler, cmd) + return create_ndarray(res, self.ndim, self.shape, self.dtype) + + def __rsub__(self, other): + pass + + def __mul__(self, other): + pass + + def __rmul__(self, other): + return self * other + + def __matmul__(self, other): + cmd = self._get_operation_command("@", other) + res = send_command(self._operation_handler, cmd) + return create_ndarray(res, res_ndim, res_shape, self.dtype) + + def to_numpy(self): + total_size = self.size * self.dtype.itemsize + cmd = self._get_fetch_command() + data_ptr = send_command(self._fetch_handler, cmd, reply_size=total_size) + data = cast(memoryview, data_ptr) + return np.frombuffer(data, np.dtype(self.dtype)).copy() + diff --git a/pyproject/server.ci b/pyproject/server.ci new file mode 100644 index 0000000..9f065a5 --- /dev/null +++ b/pyproject/server.ci @@ -0,0 +1,9 @@ +mainmodule server +{ + extern module libaum; + + mainchare Main + { + entry Main(CkArgMsg*); + }; +} diff --git a/pyproject/server.cpp b/pyproject/server.cpp new file mode 100644 index 0000000..6683d23 --- /dev/null +++ b/pyproject/server.cpp @@ -0,0 +1,127 @@ +#include +#include +#include +#include "converse.h" +#include "conv-ccs.h" + +#include "server.decl.h" + +#define REPLY_SIZE 8 + +using aum_name_t = uint64_t; + +std::unordered_map symbol_table; +aum_name_t next_name; + +class Main : public CBase_Main +{ +private: + // FIXME - how to handle different types? +public: + Main(CkArgMsg* msg) + { + next_name = 0; + register_handlers(); + } + + inline static void insert(aum_name_t name, aum::matrix const& mat) + { + symbol_table.emplace(name, mat); + } + + inline static aum_name_t get_name() + { + return next_name++; + } + + static aum::matrix& lookup(aum_name_t name) + { + auto find = symbol_table.find(name); + if (find == std::end(symbol_table)) + CmiAbort("Symbol %i not found", name); + return find->second; + } + + static void operation_handler(char* msg) + { + auto res_name = get_name(); + uint32_t* opcode = reinterpret_cast(msg); + switch(*opcode) + { + case 0: { + // no op + CmiAbort("No op not implemented"); + } + case 1: { + // addition + uint64_t* name1 = reinterpret_cast(msg + 4); + uint64_t* name2 = reinterpret_cast(msg + 12); + auto& c1 = lookup(*name1); + auto& c2 = lookup(*name2); + auto op = aum::bind::operation(c1, c2); + auto res = op.execute(aum::bind::oper::add); + insert(res_name, res); + } + default: { + CmiAbort("Operation not implemented"); + } + } + CcsSendReply(REPLY_SIZE, (void*) &res_name); + } + + static void creation_handler(char* msg) + { + /* First 32 bits are number of dimensions + * Each of the next 64 bits is the size of the array + * in each dimension + */ + // FIXME need a field for dtype + auto res_name = get_name(); + char* cmd = msg + CmiMsgHeaderSizeBytes; + uint32_t* ndim = reinterpret_cast(cmd); + switch(*ndim) + { + case 0: { + // create scalar + CmiAbort("Not implemented"); + } + case 1: { + // create vector + CmiAbort("Not implemented"); + } + case 2: { + // create matrix + uint64_t* size1 = reinterpret_cast(cmd + 4); + uint64_t* size2 = reinterpret_cast(cmd + 12); + auto res = aum::matrix(*size1, *size2); + insert(res_name, res); + break; + } + default: { + CmiAbort("Greater than 2 dimensions not supported"); + } + } + CkPrintf("Created array %lld on server\n", res_name); + CcsSendReply(REPLY_SIZE, (void*) &res_name); + } + + static void fetch_handler(char* msg) + { + CmiAbort("Fetching not implemented"); + } + + inline static void exit_server(char* msg) + { + CkExit(); + } + + void register_handlers() + { + CcsRegisterHandler("aum_operation", (CmiHandler) operation_handler); + CcsRegisterHandler("aum_creation", (CmiHandler) creation_handler); + CcsRegisterHandler("aum_fetch", (CmiHandler) fetch_handler); + CcsRegisterHandler("aum_exit", (CmiHandler) exit_server); + } +}; + +#include "server.def.h" diff --git a/setup.py b/setup.py index cd25964..1fee07e 100644 --- a/setup.py +++ b/setup.py @@ -1,28 +1,17 @@ import sys from setuptools import setup, find_packages -try: - from Cython.Distutils import Extension - from Cython.Build import cythonize -except ImportError: - from distutils.core import Extension - - def cythonize(*args, **kw): - return args[0] - def get_version(): import os data = {} - fname = os.path.join('charm.numpy', '__init__.py') + fname = os.path.join('pyproject', '__init__.py') exec(compile(open(fname).read(), fname, 'exec'), data) return data.get('__version__') -install_requires = ['cython', 'numpy', 'charm4py'] +install_requires = ['numpy', 'charm4py'] tests_require = ['pytest'] -if sys.version_info[0] < 3: - tests_require += ['mock>=1.0'] docs_require = ['sphinx'] classes = ''' @@ -44,26 +33,17 @@ def get_version(): ''' classifiers = [x.strip() for x in classes.splitlines() if x] -ext_modules = [ - Extension( - name="charm.numpy.core", - sources=["charm/numpy/core.pyx"], - language="c++" - ), - ] - setup( - name='charm.numpy', + name='pyproject', version=get_version(), author='Aditya Bhosale', author_email='adityapb1546@gmail.com', description='A python library for distributed array computations', long_description=open('README.rst').read(), license="BSD", - url='https://github.com/adityapb/charm.numpy', + url='https://github.com/UIUC-PPL/PyProject', classifiers=classifiers, packages=find_packages(), - ext_modules=cythonize(ext_modules, language="c++"), install_requires=install_requires, extras_require={ "docs": docs_require, From 4fea9fa1e03abfb74440682eaa600cbb0563ce4d Mon Sep 17 00:00:00 2001 From: Aditya Bhosale Date: Fri, 1 Apr 2022 18:59:11 -0500 Subject: [PATCH 03/16] Add array deletion and example --- examples/simple.py | 13 +++++++++++++ pyproject/array.py | 21 ++++++++++++++++----- pyproject/server.cpp | 31 ++++++++++++++++++++++++------- 3 files changed, 53 insertions(+), 12 deletions(-) create mode 100644 examples/simple.py diff --git a/examples/simple.py b/examples/simple.py new file mode 100644 index 0000000..68b2871 --- /dev/null +++ b/examples/simple.py @@ -0,0 +1,13 @@ +from pyproject.array import connect, ndarray +import numpy as np + +def f(): + x = ndarray(2, [1000, 1000], np.float64) + y = ndarray(2, [1000, 1000], np.float64) + + z = x + y + +connect("172.17.0.1", 10000) +z = f() +m = ndarray(2, [10, 10], np.float64) + diff --git a/pyproject/array.py b/pyproject/array.py index 5ada70c..b49450d 100644 --- a/pyproject/array.py +++ b/pyproject/array.py @@ -4,12 +4,14 @@ server = None +OPCODES = {'+': 1, '-': 2, '*': 3, '/': 4, '@': 5} + def create_ndarray(name, ndim, shape, dtype): return ndarray(ndim, shape, dtype, name) def send_command(handler, msg, reply_size=8): server.send_request(handler, 0, msg) - return int.from_bytes(server.receive_response(reply_size), "little") + return int.from_bytes(server.receive_response(reply_size), 'little') def connect(server_ip, server_port): global server @@ -34,6 +36,7 @@ def __init__(self, ndim, shape, dtype, name=None): self._creation_handler = b'aum_creation' self._operation_handler = b'aum_operation' self._fetch_handler = b'aum_fetch' + self._delete_handler = b'aum_delete' self._exit_handler = b'aum_exit' if name: self.name = name @@ -45,9 +48,9 @@ def _get_creation_command(self): """ Generate array creation CCS command """ - cmd = int(self.ndim).to_bytes(4, "little") + cmd = int(self.ndim).to_bytes(4, 'little') for s in self.shape: - cmd += int(s).to_bytes(8, "little") + cmd += int(s).to_bytes(8, 'little') return cmd def _get_fetch_command(self): @@ -60,10 +63,18 @@ def _get_operation_command(self, operation, other): """ Generate CCS command for operation """ - pass + if operation not in OPCODES: + raise NotImplementedError("Operation %s not supported" + % operation) + opcode = OPCODES.get(operation) + cmd = opcode.to_bytes(4, 'little') + cmd += self.name.to_bytes(8, 'little') + cmd += other.name.to_bytes(8, 'little') + return cmd def __del__(self): - pass + cmd = int(self.name).to_bytes(8, 'little') + send_command(self._delete_handler, cmd) def __len__(self): return self.shape[0] diff --git a/pyproject/server.cpp b/pyproject/server.cpp index 6683d23..45ea831 100644 --- a/pyproject/server.cpp +++ b/pyproject/server.cpp @@ -6,7 +6,7 @@ #include "server.decl.h" -#define REPLY_SIZE 8 +#define NAME_SIZE 8 using aum_name_t = uint64_t; @@ -26,9 +26,15 @@ class Main : public CBase_Main inline static void insert(aum_name_t name, aum::matrix const& mat) { + CkPrintf("Created array %lld on server\n", name); symbol_table.emplace(name, mat); } + inline static void remove(aum_name_t name) + { + symbol_table.erase(name); + } + inline static aum_name_t get_name() { return next_name++; @@ -45,7 +51,8 @@ class Main : public CBase_Main static void operation_handler(char* msg) { auto res_name = get_name(); - uint32_t* opcode = reinterpret_cast(msg); + char* cmd = msg + CmiMsgHeaderSizeBytes; + uint32_t* opcode = reinterpret_cast(cmd); switch(*opcode) { case 0: { @@ -54,19 +61,20 @@ class Main : public CBase_Main } case 1: { // addition - uint64_t* name1 = reinterpret_cast(msg + 4); - uint64_t* name2 = reinterpret_cast(msg + 12); + uint64_t* name1 = reinterpret_cast(cmd + 4); + uint64_t* name2 = reinterpret_cast(cmd + 12); auto& c1 = lookup(*name1); auto& c2 = lookup(*name2); auto op = aum::bind::operation(c1, c2); auto res = op.execute(aum::bind::oper::add); insert(res_name, res); + break; } default: { CmiAbort("Operation not implemented"); } } - CcsSendReply(REPLY_SIZE, (void*) &res_name); + CcsSendReply(NAME_SIZE, (void*) &res_name); } static void creation_handler(char* msg) @@ -101,8 +109,7 @@ class Main : public CBase_Main CmiAbort("Greater than 2 dimensions not supported"); } } - CkPrintf("Created array %lld on server\n", res_name); - CcsSendReply(REPLY_SIZE, (void*) &res_name); + CcsSendReply(NAME_SIZE, (void*) &res_name); } static void fetch_handler(char* msg) @@ -110,6 +117,15 @@ class Main : public CBase_Main CmiAbort("Fetching not implemented"); } + static void delete_handler(char* msg) + { + char* cmd = msg + CmiMsgHeaderSizeBytes; + uint64_t* name = reinterpret_cast(cmd); + remove(*name); + CkPrintf("Deleted array %lld on server\n", *name); + CcsSendReply(NAME_SIZE, (void*) name); + } + inline static void exit_server(char* msg) { CkExit(); @@ -120,6 +136,7 @@ class Main : public CBase_Main CcsRegisterHandler("aum_operation", (CmiHandler) operation_handler); CcsRegisterHandler("aum_creation", (CmiHandler) creation_handler); CcsRegisterHandler("aum_fetch", (CmiHandler) fetch_handler); + CcsRegisterHandler("aum_delete", (CmiHandler) delete_handler); CcsRegisterHandler("aum_exit", (CmiHandler) exit_server); } }; From 622b19b13ade4a973ea6c9ce6c36a3b153730a46 Mon Sep 17 00:00:00 2001 From: Aditya Bhosale Date: Wed, 6 Apr 2022 12:40:31 -0500 Subject: [PATCH 04/16] Add matrix operations and Makefile for server --- examples/simple.py | 1 + pyproject/Makefile | 17 +++++++++++++++++ pyproject/server.cpp | 22 +++++++++++++++++----- setup.py | 15 +++++++++++---- 4 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 pyproject/Makefile diff --git a/examples/simple.py b/examples/simple.py index 68b2871..b4b0d2e 100644 --- a/examples/simple.py +++ b/examples/simple.py @@ -6,6 +6,7 @@ def f(): y = ndarray(2, [1000, 1000], np.float64) z = x + y + z -= x connect("172.17.0.1", 10000) z = f() diff --git a/pyproject/Makefile b/pyproject/Makefile new file mode 100644 index 0000000..1d0ee64 --- /dev/null +++ b/pyproject/Makefile @@ -0,0 +1,17 @@ +CHARMC=/home/adityapb/charm/charm/netlrts-linux-x86_64/bin/charmc +BASE_DIR=/home/adityapb/charm/LibAum +LIBS_DIR=$(BASE_DIR) +OPTS=-c++-option -std=c++17 -O3 + +all: server + +.PHONY: clean server + +server_ci: server.ci + $(CHARMC) -E server.ci + +server: server.cpp server_ci + $(CHARMC) $< -L$(LIBS_DIR)/aum -laum -I$(BASE_DIR) -I$(BASE_DIR)/aum/backend -o $@.out $(OPTS) + +clean: + rm *.decl.h *.def.h *.out charmrun diff --git a/pyproject/server.cpp b/pyproject/server.cpp index 45ea831..877e393 100644 --- a/pyproject/server.cpp +++ b/pyproject/server.cpp @@ -53,6 +53,11 @@ class Main : public CBase_Main auto res_name = get_name(); char* cmd = msg + CmiMsgHeaderSizeBytes; uint32_t* opcode = reinterpret_cast(cmd); + uint64_t* name1 = reinterpret_cast(cmd + 4); + uint64_t* name2 = reinterpret_cast(cmd + 12); + auto& c1 = lookup(*name1); + auto& c2 = lookup(*name2); + auto op = aum::bind::operation(c1, c2); switch(*opcode) { case 0: { @@ -61,15 +66,22 @@ class Main : public CBase_Main } case 1: { // addition - uint64_t* name1 = reinterpret_cast(cmd + 4); - uint64_t* name2 = reinterpret_cast(cmd + 12); - auto& c1 = lookup(*name1); - auto& c2 = lookup(*name2); - auto op = aum::bind::operation(c1, c2); auto res = op.execute(aum::bind::oper::add); insert(res_name, res); break; } + case 2: { + // subtraction + auto res = op.execute(aum::bind::oper::sub); + insert(res_name, res); + break; + } + // case 5: { + // // matmul + // auto res = op.execute(aum::bind::oper::mul); + // insert(res_name, res); + // break; + // } default: { CmiAbort("Operation not implemented"); } diff --git a/setup.py b/setup.py index 1fee07e..2d33135 100644 --- a/setup.py +++ b/setup.py @@ -1,15 +1,23 @@ import sys +import os +import subprocess from setuptools import setup, find_packages def get_version(): - import os data = {} fname = os.path.join('pyproject', '__init__.py') exec(compile(open(fname).read(), fname, 'exec'), data) return data.get('__version__') +def compile_server(): + charmc = os.environ.get('CHARMC', '~/charm/netlrts-linux-x86_64/bin/charmc') + aum_base = os.environ.get('AUM_HOME', '~/LibAum') + subprocess.run(["make", "-C", "pyproject/", + "CHARMC=%s" % charmc, "BASE_DIR=%s" % aum_base]) + + install_requires = ['numpy', 'charm4py'] tests_require = ['pytest'] docs_require = ['sphinx'] @@ -21,18 +29,17 @@ def get_version(): License :: OSI Approved :: BSD License Natural Language :: English Operating System :: MacOS :: MacOS X -Operating System :: Microsoft :: Windows Operating System :: POSIX Operating System :: Unix Programming Language :: Python -Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 -Topic :: Scientific/Engineering Topic :: Software Development :: Libraries Topic :: Utilities ''' classifiers = [x.strip() for x in classes.splitlines() if x] +compile_server() + setup( name='pyproject', version=get_version(), From 0ff67834c76154d1e9e37c601349f5270c68b3e6 Mon Sep 17 00:00:00 2001 From: Aditya Bhosale Date: Fri, 15 Apr 2022 14:10:00 -0500 Subject: [PATCH 05/16] Add handling for different AUM array types --- examples/simple.py | 13 +++--- pyproject/Makefile | 5 ++- pyproject/array.py | 77 +++++++++++++++++++++++++++-------- pyproject/server.cpp | 97 ++++++++++++++++++++++++++++++++------------ 4 files changed, 143 insertions(+), 49 deletions(-) diff --git a/examples/simple.py b/examples/simple.py index b4b0d2e..1851b7e 100644 --- a/examples/simple.py +++ b/examples/simple.py @@ -2,13 +2,14 @@ import numpy as np def f(): - x = ndarray(2, [1000, 1000], np.float64) - y = ndarray(2, [1000, 1000], np.float64) + x = ndarray(1, 1000, np.float64) + y = ndarray(1, 1000, np.float64) + + return x @ y - z = x + y - z -= x connect("172.17.0.1", 10000) -z = f() -m = ndarray(2, [10, 10], np.float64) +s = f() +print(s.get()) + diff --git a/pyproject/Makefile b/pyproject/Makefile index 1d0ee64..324a5f9 100644 --- a/pyproject/Makefile +++ b/pyproject/Makefile @@ -5,7 +5,7 @@ OPTS=-c++-option -std=c++17 -O3 all: server -.PHONY: clean server +.PHONY: clean server.out server_ci: server.ci $(CHARMC) -E server.ci @@ -13,5 +13,8 @@ server_ci: server.ci server: server.cpp server_ci $(CHARMC) $< -L$(LIBS_DIR)/aum -laum -I$(BASE_DIR) -I$(BASE_DIR)/aum/backend -o $@.out $(OPTS) +run-server: server.out + ./charmrun +p4 ./server.out ++server ++server-port 10000 ++local + clean: rm *.decl.h *.def.h *.out charmrun diff --git a/pyproject/array.py b/pyproject/array.py index b49450d..1d7a6cb 100644 --- a/pyproject/array.py +++ b/pyproject/array.py @@ -1,4 +1,5 @@ import sys +import struct import numpy as np from pyccs import Server @@ -6,12 +7,24 @@ OPCODES = {'+': 1, '-': 2, '*': 3, '/': 4, '@': 5} +def to_bytes(value, dtype='i'): + return struct.pack(dtype, value) + +def from_bytes(bvalue, dtype='i'): + return struct.unpack(dtype, bvalue)[0] + def create_ndarray(name, ndim, shape, dtype): - return ndarray(ndim, shape, dtype, name) + return ndarray(ndim, shape, dtype, name=name) + +def send_command_raw(handler, msg, reply_size): + server.send_request(handler, 0, msg) + return server.receive_response(reply_size) def send_command(handler, msg, reply_size=8): + return from_bytes(send_command_raw(handler, msg, reply_size), 'l') + +def send_command_async(handler, msg): server.send_request(handler, 0, msg) - return int.from_bytes(server.receive_response(reply_size), 'little') def connect(server_ip, server_port): global server @@ -19,7 +32,7 @@ def connect(server_ip, server_port): server.connect() class ndarray: - def __init__(self, ndim, shape, dtype, name=None): + def __init__(self, ndim, shape, dtype, value=None, name=None): """ This is the wrapper class for AUM array objects. The argument 'name' should be None except when wrapping @@ -31,7 +44,12 @@ def __init__(self, ndim, shape, dtype, name=None): self.dtype = dtype self.ndim = ndim self.itemsize = np.dtype(dtype).itemsize - self.shape = np.asarray(shape, dtype=np.int32) + if self.ndim == 0 and value: + self.value = value + if isinstance(shape, np.ndarray) or isinstance(shape, list): + self.shape = np.asarray(shape, dtype=np.int32) + else: + self.shape = np.asarray([shape], dtype=np.int32) self.size = np.prod(self.shape) self._creation_handler = b'aum_creation' self._operation_handler = b'aum_operation' @@ -48,16 +66,17 @@ def _get_creation_command(self): """ Generate array creation CCS command """ - cmd = int(self.ndim).to_bytes(4, 'little') + cmd = to_bytes(self.ndim, 'i') for s in self.shape: - cmd += int(s).to_bytes(8, 'little') + cmd += to_bytes(s, 'l') return cmd def _get_fetch_command(self): """ Generate CCS command to fetch entire array data """ - pass + cmd = to_bytes(self.name, 'l') + return cmd def _get_operation_command(self, operation, other): """ @@ -67,14 +86,14 @@ def _get_operation_command(self, operation, other): raise NotImplementedError("Operation %s not supported" % operation) opcode = OPCODES.get(operation) - cmd = opcode.to_bytes(4, 'little') - cmd += self.name.to_bytes(8, 'little') - cmd += other.name.to_bytes(8, 'little') + cmd = to_bytes(opcode, 'i') + cmd += to_bytes(self.name, 'l') + cmd += to_bytes(other.name, 'l') return cmd def __del__(self): - cmd = int(self.name).to_bytes(8, 'little') - send_command(self._delete_handler, cmd) + cmd = to_bytes(self.name, 'l') + send_command_async(self._delete_handler, cmd) def __len__(self): return self.shape[0] @@ -114,14 +133,38 @@ def __rmul__(self, other): return self * other def __matmul__(self, other): + if self.ndim == 2 and other.ndim == 2: + res_ndim = 2 + if self.shape[1] != other.shape[0]: + raise RuntimeError("Shape mismatch") + res_shape = np.array([self.shape[0], other.shape[1]], + dtype=np.int32) + elif self.ndim == 2 and other.ndim == 1: + res_ndim = 1 + if self.shape[1] != other.shape[0]: + raise RuntimeError("Shape mismatch") + res_shape = np.array([self.shape[0]], dtype=np.int32) + elif self.ndim == 1 and other.ndim == 1: + res_ndim = 0 + if self.shape[0] != other.shape[0]: + raise RuntimeError("Shape mismatch") + res_shape = np.array([], dtype=np.int32) + else: + raise RuntimeError("Dimension mismatch") + cmd = self._get_operation_command("@", other) res = send_command(self._operation_handler, cmd) return create_ndarray(res, res_ndim, res_shape, self.dtype) - def to_numpy(self): - total_size = self.size * self.dtype.itemsize + def get(self): cmd = self._get_fetch_command() - data_ptr = send_command(self._fetch_handler, cmd, reply_size=total_size) - data = cast(memoryview, data_ptr) - return np.frombuffer(data, np.dtype(self.dtype)).copy() + if self.ndim == 0: + total_size = self.itemsize + data_bytes = send_command_raw(self._fetch_handler, cmd, reply_size=total_size) + return from_bytes(data_bytes, np.dtype(self.dtype).char) + else: + total_size = self.size * self.itemsize + data_ptr = send_command(self._fetch_handler, cmd, reply_size=total_size) + data = cast(memoryview, data_ptr) + return np.frombuffer(data, np.dtype(self.dtype)).copy() diff --git a/pyproject/server.cpp b/pyproject/server.cpp index 877e393..38ac714 100644 --- a/pyproject/server.cpp +++ b/pyproject/server.cpp @@ -1,6 +1,6 @@ #include -#include #include +#include #include "converse.h" #include "conv-ccs.h" @@ -9,25 +9,23 @@ #define NAME_SIZE 8 using aum_name_t = uint64_t; +using aum_array_t = std::variant; -std::unordered_map symbol_table; -aum_name_t next_name; +std::unordered_map symbol_table; +aum_name_t next_name = 0; class Main : public CBase_Main { -private: - // FIXME - how to handle different types? public: Main(CkArgMsg* msg) { - next_name = 0; register_handlers(); } - inline static void insert(aum_name_t name, aum::matrix const& mat) + inline static void insert(aum_name_t name, aum_array_t arr) { CkPrintf("Created array %lld on server\n", name); - symbol_table.emplace(name, mat); + symbol_table.emplace(name, arr); } inline static void remove(aum_name_t name) @@ -40,7 +38,7 @@ class Main : public CBase_Main return next_name++; } - static aum::matrix& lookup(aum_name_t name) + static aum_array_t& lookup(aum_name_t name) { auto find = symbol_table.find(name); if (find == std::end(symbol_table)) @@ -55,9 +53,8 @@ class Main : public CBase_Main uint32_t* opcode = reinterpret_cast(cmd); uint64_t* name1 = reinterpret_cast(cmd + 4); uint64_t* name2 = reinterpret_cast(cmd + 12); - auto& c1 = lookup(*name1); - auto& c2 = lookup(*name2); - auto op = aum::bind::operation(c1, c2); + aum_array_t& c1 = lookup(*name1); + aum_array_t& c2 = lookup(*name2); switch(*opcode) { case 0: { @@ -66,22 +63,53 @@ class Main : public CBase_Main } case 1: { // addition - auto res = op.execute(aum::bind::oper::add); - insert(res_name, res); + std::visit( + [&](auto& x, auto& y) { + using T = std::decay_t; + using V = std::decay_t; + aum_array_t res; + if constexpr(std::is_same_v) + res = x + y; + else + CmiAbort("Operation not permitted"); + insert(res_name, res); + }, c1, c2); break; } case 2: { // subtraction - auto res = op.execute(aum::bind::oper::sub); - insert(res_name, res); + std::visit( + [&](auto& x, auto& y) { + using T = std::decay_t; + using V = std::decay_t; + aum_array_t res; + if constexpr(std::is_same_v) + res = x - y; + else + CmiAbort("Operation not permitted"); + insert(res_name, res); + }, c1, c2); + break; + } + case 5: { + // mat/vec mul + std::visit( + [&](auto& x, auto& y) { + using T = std::decay_t; + using V = std::decay_t; + aum_array_t res; + if constexpr(std::is_same_v && std::is_same_v) + // Matmul here + CmiAbort("Matrix multiplication not yet implemented"); + else if constexpr((std::is_same_v || + std::is_same_v) && std::is_same_v) + res = aum::dot(x, y); + else + CmiAbort("Operation not permitted"); + insert(res_name, res); + }, c1, c2); break; } - // case 5: { - // // matmul - // auto res = op.execute(aum::bind::oper::mul); - // insert(res_name, res); - // break; - // } default: { CmiAbort("Operation not implemented"); } @@ -107,7 +135,10 @@ class Main : public CBase_Main } case 1: { // create vector - CmiAbort("Not implemented"); + uint64_t* size = reinterpret_cast(cmd + 4); + auto res = aum::vector(*size); + insert(res_name, res); + break; } case 2: { // create matrix @@ -126,7 +157,24 @@ class Main : public CBase_Main static void fetch_handler(char* msg) { - CmiAbort("Fetching not implemented"); + char* cmd = msg + CmiMsgHeaderSizeBytes; + aum_name_t* name = reinterpret_cast(cmd); + aum_array_t arr = lookup(*name); + void* reply = nullptr; + int reply_size = 0; + std::visit( + [&](auto& x) { + using T = std::decay_t; + if constexpr(std::is_same_v) + { + double value = x.get(); + reply = (void*) &value; + reply_size += 8; + } + else + CmiAbort("Operation not implemented"); + }, arr); + CcsSendReply(reply_size, reply); } static void delete_handler(char* msg) @@ -135,7 +183,6 @@ class Main : public CBase_Main uint64_t* name = reinterpret_cast(cmd); remove(*name); CkPrintf("Deleted array %lld on server\n", *name); - CcsSendReply(NAME_SIZE, (void*) name); } inline static void exit_server(char* msg) From 1e54596459df27245d54fd59c151f588ecb94bff Mon Sep 17 00:00:00 2001 From: Aditya Bhosale Date: Tue, 26 Apr 2022 16:37:12 -0500 Subject: [PATCH 06/16] Add more operations --- examples/simple.py | 16 +++++---- pyproject/array.py | 40 ++++++++++++++++----- pyproject/server.cpp | 82 +++++++++++++++++++++++++++++++++++++++----- 3 files changed, 115 insertions(+), 23 deletions(-) diff --git a/examples/simple.py b/examples/simple.py index 1851b7e..30f7d96 100644 --- a/examples/simple.py +++ b/examples/simple.py @@ -2,14 +2,18 @@ import numpy as np def f(): - x = ndarray(1, 1000, np.float64) - y = ndarray(1, 1000, np.float64) + x = ndarray(1, 1000, np.float64, init_value=1.) + y = ndarray(1, 1000, np.float64, init_value=1.) - return x @ y + a = ndarray(1, 100, np.float64, init_value=1.) + b = ndarray(1, 100, np.float64, init_value=1.) + return (x @ y) / (a @ b) -connect("172.17.0.1", 10000) -s = f() -print(s.get()) + +if __name__ == '__main__': + connect("172.17.0.1", 10000) + s = f() + print(s.get()) diff --git a/pyproject/array.py b/pyproject/array.py index 1d7a6cb..1282b9f 100644 --- a/pyproject/array.py +++ b/pyproject/array.py @@ -5,7 +5,7 @@ server = None -OPCODES = {'+': 1, '-': 2, '*': 3, '/': 4, '@': 5} +OPCODES = {'+': 1, '-': 2, '*': 3, '/': 4, '@': 5, 'copy': 6} def to_bytes(value, dtype='i'): return struct.pack(dtype, value) @@ -32,7 +32,7 @@ def connect(server_ip, server_port): server.connect() class ndarray: - def __init__(self, ndim, shape, dtype, value=None, name=None): + def __init__(self, ndim, shape, dtype, init_value=None, name=None): """ This is the wrapper class for AUM array objects. The argument 'name' should be None except when wrapping @@ -44,15 +44,15 @@ def __init__(self, ndim, shape, dtype, value=None, name=None): self.dtype = dtype self.ndim = ndim self.itemsize = np.dtype(dtype).itemsize - if self.ndim == 0 and value: - self.value = value + self.init_value = init_value if isinstance(shape, np.ndarray) or isinstance(shape, list): self.shape = np.asarray(shape, dtype=np.int32) else: self.shape = np.asarray([shape], dtype=np.int32) self.size = np.prod(self.shape) self._creation_handler = b'aum_creation' - self._operation_handler = b'aum_operation' + self._binop_handler = b'aum_binop' + self._monop_handler = b'aum_monop' self._fetch_handler = b'aum_fetch' self._delete_handler = b'aum_delete' self._exit_handler = b'aum_exit' @@ -67,8 +67,11 @@ def _get_creation_command(self): Generate array creation CCS command """ cmd = to_bytes(self.ndim, 'i') + cmd += to_bytes(self.init_value is not None, '?') for s in self.shape: cmd += to_bytes(s, 'l') + if self.init_value is not None: + cmd += to_bytes(self.init_value, 'd') return cmd def _get_fetch_command(self): @@ -91,6 +94,15 @@ def _get_operation_command(self, operation, other): cmd += to_bytes(other.name, 'l') return cmd + def _get_copy_command(self): + """ + Generate CCS command for copy + """ + opcode = OPCODES.get('copy') + cmd = to_bytes(opcode, 'i') + cmd += to_bytes(self.name, 'l') + return cmd + def __del__(self): cmd = to_bytes(self.name, 'l') send_command_async(self._delete_handler, cmd) @@ -109,7 +121,7 @@ def __add__(self, other): raise RuntimeError("Shape mismatch %s and %s" % (self.shape, other.shape)) cmd = self._get_operation_command("+", other) - res = send_command(self._operation_handler, cmd) + res = send_command(self._binop_handler, cmd) return create_ndarray(res, self.ndim, self.shape, self.dtype) def __radd__(self, other): @@ -120,7 +132,7 @@ def __sub__(self, other): raise RuntimeError("Shape mismatch %s and %s" % (self.shape, other.shape)) cmd = self._get_operation_command("-", other) - res = send_command(self._operation_handler, cmd) + res = send_command(self._binop_handler, cmd) return create_ndarray(res, self.ndim, self.shape, self.dtype) def __rsub__(self, other): @@ -132,6 +144,13 @@ def __mul__(self, other): def __rmul__(self, other): return self * other + def __truediv__(self, other): + if self.ndim > 0 or other.ndim > 0: + RuntimeError("Cannote divide two arrays") + cmd = self._get_operation_command("/", other) + res = send_command(self._binop_handler, cmd) + return create_ndarray(res, self.ndim, self.shape, self.dtype) + def __matmul__(self, other): if self.ndim == 2 and other.ndim == 2: res_ndim = 2 @@ -153,7 +172,7 @@ def __matmul__(self, other): raise RuntimeError("Dimension mismatch") cmd = self._get_operation_command("@", other) - res = send_command(self._operation_handler, cmd) + res = send_command(self._binop_handler, cmd) return create_ndarray(res, res_ndim, res_shape, self.dtype) def get(self): @@ -168,3 +187,8 @@ def get(self): data = cast(memoryview, data_ptr) return np.frombuffer(data, np.dtype(self.dtype)).copy() + def copy(self): + cmd = self._get_copy_command() + res = send_command(self._monop_handler, cmd) + return create_ndarray(res, self.ndim, self.shape, self.dtype) + diff --git a/pyproject/server.cpp b/pyproject/server.cpp index 38ac714..2e7d3ef 100644 --- a/pyproject/server.cpp +++ b/pyproject/server.cpp @@ -46,7 +46,36 @@ class Main : public CBase_Main return find->second; } - static void operation_handler(char* msg) + static void monop_handler(char* msg) + { + auto res_name = get_name(); + char* cmd = msg + CmiMsgHeaderSizeBytes; + uint32_t* opcode = reinterpret_cast(cmd); + uint64_t* name1 = reinterpret_cast(cmd + 4); + aum_array_t& c1 = lookup(*name1); + switch(*opcode) + { + // FIXME implement a matrix copy + case 6: { + std::visit( + [&](auto& x) { + using T = std::decay_t; + if constexpr(std::is_same_v || std::is_same_v) + { + aum_array_t res = aum::copy(x); + insert(res_name, res); + } + else + { + CmiAbort("Matrix copy not implemented!"); + } + }, c1); + } + } + CcsSendReply(NAME_SIZE, (void*) &res_name); + } + + static void binop_handler(char* msg) { auto res_name = get_name(); char* cmd = msg + CmiMsgHeaderSizeBytes; @@ -91,6 +120,21 @@ class Main : public CBase_Main }, c1, c2); break; } + case 4: { + // division + std::visit( + [&](auto& x, auto& y) { + using T = std::decay_t; + using V = std::decay_t; + aum_array_t res; + if constexpr(std::is_same_v && std::is_same_v) + res = x / y; + else + CmiAbort("Operation not permitted"); + insert(res_name, res); + }, c1, c2); + break; + } case 5: { // mat/vec mul std::visit( @@ -127,6 +171,7 @@ class Main : public CBase_Main auto res_name = get_name(); char* cmd = msg + CmiMsgHeaderSizeBytes; uint32_t* ndim = reinterpret_cast(cmd); + bool* has_init = reinterpret_cast(cmd + 4); switch(*ndim) { case 0: { @@ -135,16 +180,34 @@ class Main : public CBase_Main } case 1: { // create vector - uint64_t* size = reinterpret_cast(cmd + 4); - auto res = aum::vector(*size); + uint64_t* size = reinterpret_cast(cmd + 5); + aum_array_t res; + if (*has_init) + { + double* init_value = reinterpret_cast(cmd + 13); + res = aum::vector(*size, *init_value); + } + else + { + res = aum::vector(*size); + } insert(res_name, res); break; } case 2: { // create matrix - uint64_t* size1 = reinterpret_cast(cmd + 4); - uint64_t* size2 = reinterpret_cast(cmd + 12); - auto res = aum::matrix(*size1, *size2); + uint64_t* size1 = reinterpret_cast(cmd + 5); + uint64_t* size2 = reinterpret_cast(cmd + 13); + aum_array_t res; + if (*has_init) + { + double* init_value = reinterpret_cast(cmd + 21); + res = aum::matrix(*size1, *size2, *init_value); + } + else + { + res = aum::matrix(*size1, *size2); + } insert(res_name, res); break; } @@ -159,7 +222,7 @@ class Main : public CBase_Main { char* cmd = msg + CmiMsgHeaderSizeBytes; aum_name_t* name = reinterpret_cast(cmd); - aum_array_t arr = lookup(*name); + aum_array_t& arr = lookup(*name); void* reply = nullptr; int reply_size = 0; std::visit( @@ -170,11 +233,11 @@ class Main : public CBase_Main double value = x.get(); reply = (void*) &value; reply_size += 8; + CcsSendReply(reply_size, reply); } else CmiAbort("Operation not implemented"); }, arr); - CcsSendReply(reply_size, reply); } static void delete_handler(char* msg) @@ -192,7 +255,8 @@ class Main : public CBase_Main void register_handlers() { - CcsRegisterHandler("aum_operation", (CmiHandler) operation_handler); + CcsRegisterHandler("aum_binop", (CmiHandler) binop_handler); + CcsRegisterHandler("aum_monop", (CmiHandler) monop_handler); CcsRegisterHandler("aum_creation", (CmiHandler) creation_handler); CcsRegisterHandler("aum_fetch", (CmiHandler) fetch_handler); CcsRegisterHandler("aum_delete", (CmiHandler) delete_handler); From 14341809459a6018bc4e81fcdc4a0a27d785bb32 Mon Sep 17 00:00:00 2001 From: Aditya Bhosale Date: Thu, 28 Apr 2022 17:25:17 -0500 Subject: [PATCH 07/16] Add more operations and conjugate gradient example --- examples/conjugate_gradient.py | 36 +++++++ examples/simple.py | 13 +-- pyproject/array.py | 131 +++++++----------------- pyproject/ccs.py | 72 ++++++++++++++ pyproject/linalg.py | 19 ++++ pyproject/server.cpp | 177 ++++++++++++++++++++++++++------- 6 files changed, 311 insertions(+), 137 deletions(-) create mode 100644 examples/conjugate_gradient.py create mode 100644 pyproject/ccs.py create mode 100644 pyproject/linalg.py diff --git a/examples/conjugate_gradient.py b/examples/conjugate_gradient.py new file mode 100644 index 0000000..a9c2f35 --- /dev/null +++ b/examples/conjugate_gradient.py @@ -0,0 +1,36 @@ +from pyproject.array import connect, ndarray +import pyproject.linalg as lg +import numpy as np + +def solve(A, b): + x = ndarray(1, 100, np.float64) + r = b - A @ x + p = r.copy() + rsold = r @ r + + for i in range(100): + Ap = A @ p + alpha = rsold / (p @ Ap) + + x = lg.axpy(alpha, p, x) + r = lg.axpy(alpha, Ap, r, multiplier=-1.) + + rsnew = r @ r + + if np.sqrt(rsnew.get()) < 1e-8: + print("Converged in %i iterations" % (i + 1)) + break + + p = lg.axpy(rsnew / rsold, p, r) + rsold = rsnew + + return x + +if __name__ == '__main__': + connect("172.17.0.1", 10000) + + A = ndarray(2, (100, 100), np.float64, init_value=1.) + b = ndarray(1, 100, np.float64, init_value=1.) + x = solve(A, b) + + diff --git a/examples/simple.py b/examples/simple.py index 30f7d96..1115e5f 100644 --- a/examples/simple.py +++ b/examples/simple.py @@ -1,19 +1,20 @@ from pyproject.array import connect, ndarray +import pyproject.linalg as lg import numpy as np def f(): - x = ndarray(1, 1000, np.float64, init_value=1.) - y = ndarray(1, 1000, np.float64, init_value=1.) + x = ndarray(1, 10, np.float64, init_value=1.) + y = ndarray(1, 10, np.float64, init_value=1.) - a = ndarray(1, 100, np.float64, init_value=1.) - b = ndarray(1, 100, np.float64, init_value=1.) + a = ndarray(1, 10, np.float64, init_value=1.) + b = ndarray(1, 10, np.float64, init_value=1.) - return (x @ y) / (a @ b) + a = x @ y + r = lg.axpy(a, x, y, multiplier=-1.) if __name__ == '__main__': connect("172.17.0.1", 10000) s = f() - print(s.get()) diff --git a/pyproject/array.py b/pyproject/array.py index 1282b9f..b4a59de 100644 --- a/pyproject/array.py +++ b/pyproject/array.py @@ -1,36 +1,12 @@ import sys -import struct import numpy as np -from pyccs import Server - -server = None - -OPCODES = {'+': 1, '-': 2, '*': 3, '/': 4, '@': 5, 'copy': 6} - -def to_bytes(value, dtype='i'): - return struct.pack(dtype, value) - -def from_bytes(bvalue, dtype='i'): - return struct.unpack(dtype, bvalue)[0] +from pyproject.ccs import to_bytes, from_bytes, send_command_raw, send_command, \ + send_command_async, connect, get_creation_command, get_operation_command, \ + get_fetch_command, Handlers def create_ndarray(name, ndim, shape, dtype): return ndarray(ndim, shape, dtype, name=name) -def send_command_raw(handler, msg, reply_size): - server.send_request(handler, 0, msg) - return server.receive_response(reply_size) - -def send_command(handler, msg, reply_size=8): - return from_bytes(send_command_raw(handler, msg, reply_size), 'l') - -def send_command_async(handler, msg): - server.send_request(handler, 0, msg) - -def connect(server_ip, server_port): - global server - server = Server(server_ip, server_port) - server.connect() - class ndarray: def __init__(self, ndim, shape, dtype, init_value=None, name=None): """ @@ -45,67 +21,21 @@ def __init__(self, ndim, shape, dtype, init_value=None, name=None): self.ndim = ndim self.itemsize = np.dtype(dtype).itemsize self.init_value = init_value - if isinstance(shape, np.ndarray) or isinstance(shape, list): + if isinstance(shape, np.ndarray) or isinstance(shape, list) or \ + isinstance(shape, tuple): self.shape = np.asarray(shape, dtype=np.int32) else: self.shape = np.asarray([shape], dtype=np.int32) self.size = np.prod(self.shape) - self._creation_handler = b'aum_creation' - self._binop_handler = b'aum_binop' - self._monop_handler = b'aum_monop' - self._fetch_handler = b'aum_fetch' - self._delete_handler = b'aum_delete' - self._exit_handler = b'aum_exit' if name: self.name = name else: - cmd = self._get_creation_command() - self.name = send_command(self._creation_handler, cmd) - - def _get_creation_command(self): - """ - Generate array creation CCS command - """ - cmd = to_bytes(self.ndim, 'i') - cmd += to_bytes(self.init_value is not None, '?') - for s in self.shape: - cmd += to_bytes(s, 'l') - if self.init_value is not None: - cmd += to_bytes(self.init_value, 'd') - return cmd - - def _get_fetch_command(self): - """ - Generate CCS command to fetch entire array data - """ - cmd = to_bytes(self.name, 'l') - return cmd - - def _get_operation_command(self, operation, other): - """ - Generate CCS command for operation - """ - if operation not in OPCODES: - raise NotImplementedError("Operation %s not supported" - % operation) - opcode = OPCODES.get(operation) - cmd = to_bytes(opcode, 'i') - cmd += to_bytes(self.name, 'l') - cmd += to_bytes(other.name, 'l') - return cmd - - def _get_copy_command(self): - """ - Generate CCS command for copy - """ - opcode = OPCODES.get('copy') - cmd = to_bytes(opcode, 'i') - cmd += to_bytes(self.name, 'l') - return cmd + cmd = get_creation_command(self) + self.name = send_command(Handlers.creation_handler, cmd) def __del__(self): cmd = to_bytes(self.name, 'l') - send_command_async(self._delete_handler, cmd) + send_command_async(Handlers.delete_handler, cmd) def __len__(self): return self.shape[0] @@ -116,12 +46,15 @@ def __str__(self): def __repr__(self): pass + def __neg__(self): + return self * -1 + def __add__(self, other): if (self.shape != other.shape).any(): raise RuntimeError("Shape mismatch %s and %s" % (self.shape, other.shape)) - cmd = self._get_operation_command("+", other) - res = send_command(self._binop_handler, cmd) + cmd = get_operation_command("+", [self, other]) + res = send_command(Handlers.operation_handler, cmd) return create_ndarray(res, self.ndim, self.shape, self.dtype) def __radd__(self, other): @@ -131,15 +64,23 @@ def __sub__(self, other): if (self.shape != other.shape).any(): raise RuntimeError("Shape mismatch %s and %s" % (self.shape, other.shape)) - cmd = self._get_operation_command("-", other) - res = send_command(self._binop_handler, cmd) + cmd = get_operation_command("-", [self, other]) + res = send_command(Handlers.operation_handler, cmd) return create_ndarray(res, self.ndim, self.shape, self.dtype) def __rsub__(self, other): - pass + return -1 * (self - other) def __mul__(self, other): - pass + if self.ndim > 0 or (isinstance(other, ndarray) and other.ndim > 0): + RuntimeError("Cannote multiply two arrays") + if isinstance(other, ndarray): + op = '*' + elif isinstance(other, float) or isinstance(other, int): + op = '*s' + cmd = get_operation_command(op, [self, other]) + res = send_command(Handlers.operation_handler, cmd) + return create_ndarray(res, self.ndim, self.shape, self.dtype) def __rmul__(self, other): return self * other @@ -147,8 +88,12 @@ def __rmul__(self, other): def __truediv__(self, other): if self.ndim > 0 or other.ndim > 0: RuntimeError("Cannote divide two arrays") - cmd = self._get_operation_command("/", other) - res = send_command(self._binop_handler, cmd) + if isinstance(other, ndarray): + op = '/' + elif isinstance(other, float) or isinstance(other, int): + op = '/s' + cmd = get_operation_command(op, [self, other]) + res = send_command(Handlers.operation_handler, cmd) return create_ndarray(res, self.ndim, self.shape, self.dtype) def __matmul__(self, other): @@ -171,24 +116,24 @@ def __matmul__(self, other): else: raise RuntimeError("Dimension mismatch") - cmd = self._get_operation_command("@", other) - res = send_command(self._binop_handler, cmd) + cmd = get_operation_command("@", [self, other]) + res = send_command(Handlers.operation_handler, cmd) return create_ndarray(res, res_ndim, res_shape, self.dtype) def get(self): - cmd = self._get_fetch_command() + cmd = get_fetch_command(self) if self.ndim == 0: total_size = self.itemsize - data_bytes = send_command_raw(self._fetch_handler, cmd, reply_size=total_size) + data_bytes = send_command_raw(Handlers.fetch_handler, cmd, reply_size=total_size) return from_bytes(data_bytes, np.dtype(self.dtype).char) else: total_size = self.size * self.itemsize - data_ptr = send_command(self._fetch_handler, cmd, reply_size=total_size) + data_ptr = send_command(Handlers.fetch_handler, cmd, reply_size=total_size) data = cast(memoryview, data_ptr) return np.frombuffer(data, np.dtype(self.dtype)).copy() def copy(self): - cmd = self._get_copy_command() - res = send_command(self._monop_handler, cmd) + cmd = get_operation_command("copy", [self]) + res = send_command(Handlers.operation_handler, cmd) return create_ndarray(res, self.ndim, self.shape, self.dtype) diff --git a/pyproject/ccs.py b/pyproject/ccs.py new file mode 100644 index 0000000..6b88d83 --- /dev/null +++ b/pyproject/ccs.py @@ -0,0 +1,72 @@ +import struct +from pyccs import Server +from pyproject import array + +server = None + +OPCODES = {'+': 1, '-': 2, '*': 3 ,'/': 4, '@': 5, 'copy': 6, 'axpy': 7, + 'axpy_multiplier': 8, '*s': 9, '/s': 10} + +def to_bytes(value, dtype='i'): + return struct.pack(dtype, value) + +def from_bytes(bvalue, dtype='i'): + return struct.unpack(dtype, bvalue)[0] + +def send_command_raw(handler, msg, reply_size): + server.send_request(handler, 0, msg) + return server.receive_response(reply_size) + +def send_command(handler, msg, reply_size=8): + return from_bytes(send_command_raw(handler, msg, reply_size), 'l') + +def send_command_async(handler, msg): + server.send_request(handler, 0, msg) + +def connect(server_ip, server_port): + global server + server = Server(server_ip, server_port) + server.connect() + +def get_creation_command(arr): + """ + Generate array creation CCS command + """ + cmd = to_bytes(arr.ndim, 'i') + cmd += to_bytes(arr.init_value is not None, '?') + for s in arr.shape: + cmd += to_bytes(int(s), 'l') + if arr.init_value is not None: + cmd += to_bytes(arr.init_value, 'd') + return cmd + +def get_fetch_command(arr): + """ + Generate CCS command to fetch entire array data + """ + cmd = to_bytes(arr.name, 'l') + return cmd + +def get_operation_command(operation, operands): + """ + Generate CCS command for operation + """ + if operation not in OPCODES: + raise NotImplementedError("Operation %s not supported" + % operation) + opcode = OPCODES.get(operation) + cmd = to_bytes(opcode, 'i') + for x in operands: + if isinstance(x, array.ndarray): + cmd += to_bytes(x.name, 'l') + elif isinstance(x, float) or isinstance(x, int): + cmd += to_bytes(x, 'd') + return cmd + +class Handlers(object): + creation_handler = b'aum_creation' + operation_handler = b'aum_operation' + fetch_handler = b'aum_fetch' + delete_handler = b'aum_delete' + exit_handler = b'aum_exit' + diff --git a/pyproject/linalg.py b/pyproject/linalg.py new file mode 100644 index 0000000..f082f5a --- /dev/null +++ b/pyproject/linalg.py @@ -0,0 +1,19 @@ +import sys +import struct +import numpy as np +from pyccs import Server +from pyproject.ccs import send_command, get_operation_command, Handlers +from pyproject.array import create_ndarray + + +def axpy(a, x, y, multiplier=None): + operands = [a, x, y] + if multiplier is not None: + operands.append(multiplier) + operation = 'axpy_multiplier' + else: + operation = 'axpy' + cmd = get_operation_command(operation, operands) + res = send_command(Handlers.operation_handler, cmd) + return create_ndarray(res, x.ndim, x.shape, x.dtype) + diff --git a/pyproject/server.cpp b/pyproject/server.cpp index 2e7d3ef..8177179 100644 --- a/pyproject/server.cpp +++ b/pyproject/server.cpp @@ -46,44 +46,11 @@ class Main : public CBase_Main return find->second; } - static void monop_handler(char* msg) + static void operation_handler(char* msg) { auto res_name = get_name(); char* cmd = msg + CmiMsgHeaderSizeBytes; uint32_t* opcode = reinterpret_cast(cmd); - uint64_t* name1 = reinterpret_cast(cmd + 4); - aum_array_t& c1 = lookup(*name1); - switch(*opcode) - { - // FIXME implement a matrix copy - case 6: { - std::visit( - [&](auto& x) { - using T = std::decay_t; - if constexpr(std::is_same_v || std::is_same_v) - { - aum_array_t res = aum::copy(x); - insert(res_name, res); - } - else - { - CmiAbort("Matrix copy not implemented!"); - } - }, c1); - } - } - CcsSendReply(NAME_SIZE, (void*) &res_name); - } - - static void binop_handler(char* msg) - { - auto res_name = get_name(); - char* cmd = msg + CmiMsgHeaderSizeBytes; - uint32_t* opcode = reinterpret_cast(cmd); - uint64_t* name1 = reinterpret_cast(cmd + 4); - uint64_t* name2 = reinterpret_cast(cmd + 12); - aum_array_t& c1 = lookup(*name1); - aum_array_t& c2 = lookup(*name2); switch(*opcode) { case 0: { @@ -92,6 +59,10 @@ class Main : public CBase_Main } case 1: { // addition + uint64_t* name1 = reinterpret_cast(cmd + 4); + uint64_t* name2 = reinterpret_cast(cmd + 12); + aum_array_t& c1 = lookup(*name1); + aum_array_t& c2 = lookup(*name2); std::visit( [&](auto& x, auto& y) { using T = std::decay_t; @@ -107,6 +78,10 @@ class Main : public CBase_Main } case 2: { // subtraction + uint64_t* name1 = reinterpret_cast(cmd + 4); + uint64_t* name2 = reinterpret_cast(cmd + 12); + aum_array_t& c1 = lookup(*name1); + aum_array_t& c2 = lookup(*name2); std::visit( [&](auto& x, auto& y) { using T = std::decay_t; @@ -120,8 +95,31 @@ class Main : public CBase_Main }, c1, c2); break; } + case 3: { + // multiply + uint64_t* name1 = reinterpret_cast(cmd + 4); + uint64_t* name2 = reinterpret_cast(cmd + 12); + aum_array_t& c1 = lookup(*name1); + aum_array_t& c2 = lookup(*name2); + std::visit( + [&](auto& x, auto& y) { + using T = std::decay_t; + using V = std::decay_t; + aum_array_t res; + if constexpr(std::is_same_v) + res = x * y; + else + CmiAbort("Operation not permitted"); + insert(res_name, res); + }, c1, c2); + break; + } case 4: { // division + uint64_t* name1 = reinterpret_cast(cmd + 4); + uint64_t* name2 = reinterpret_cast(cmd + 12); + aum_array_t& c1 = lookup(*name1); + aum_array_t& c2 = lookup(*name2); std::visit( [&](auto& x, auto& y) { using T = std::decay_t; @@ -137,6 +135,10 @@ class Main : public CBase_Main } case 5: { // mat/vec mul + uint64_t* name1 = reinterpret_cast(cmd + 4); + uint64_t* name2 = reinterpret_cast(cmd + 12); + aum_array_t& c1 = lookup(*name1); + aum_array_t& c2 = lookup(*name2); std::visit( [&](auto& x, auto& y) { using T = std::decay_t; @@ -154,6 +156,106 @@ class Main : public CBase_Main }, c1, c2); break; } + // FIXME implement a matrix copy + case 6: { + uint64_t* name1 = reinterpret_cast(cmd + 4); + aum_array_t& c1 = lookup(*name1); + std::visit( + [&](auto& x) { + using T = std::decay_t; + if constexpr(std::is_same_v || std::is_same_v) + { + aum_array_t res = aum::copy(x); + insert(res_name, res); + } + else + { + CmiAbort("Matrix copy not implemented!"); + } + }, c1); + break; + } + case 7: { + // axpy + uint64_t* name1 = reinterpret_cast(cmd + 4); + uint64_t* name2 = reinterpret_cast(cmd + 12); + uint64_t* name3 = reinterpret_cast(cmd + 20); + aum_array_t& c1 = lookup(*name1); + aum_array_t& c2 = lookup(*name2); + aum_array_t& c3 = lookup(*name3); + std::visit( + [&](auto& a, auto& x, auto& y) { + using S = std::decay_t; + using T = std::decay_t; + using V = std::decay_t; + aum_array_t res; + if constexpr(std::is_same_v && std::is_same_v && + std::is_same_v) + res = aum::blas::axpy(a, x, y); + else + CmiAbort("Operation not permitted"); + insert(res_name, res); + }, c1, c2, c3); + break; + } + case 8: { + // axpy multiplier + uint64_t* name1 = reinterpret_cast(cmd + 4); + uint64_t* name2 = reinterpret_cast(cmd + 12); + uint64_t* name3 = reinterpret_cast(cmd + 20); + double* multiplier = reinterpret_cast(cmd + 28); + CkPrintf("Multiplier = %f\n", *multiplier); + aum_array_t& c1 = lookup(*name1); + aum_array_t& c2 = lookup(*name2); + aum_array_t& c3 = lookup(*name3); + std::visit( + [&](auto& a, auto& x, auto& y) { + using S = std::decay_t; + using T = std::decay_t; + using V = std::decay_t; + aum_array_t res; + if constexpr(std::is_same_v && std::is_same_v && + std::is_same_v) + res = aum::blas::axpy(*multiplier, a, x, y); + else + CmiAbort("Operation not permitted"); + insert(res_name, res); + }, c1, c2, c3); + break; + } + case 9: { + // multiply + uint64_t* name1 = reinterpret_cast(cmd + 4); + double* y = reinterpret_cast(cmd + 12); + aum_array_t& c1 = lookup(*name1); + std::visit( + [&](auto& x) { + using T = std::decay_t; + aum_array_t res; + res = *y * x; + insert(res_name, res); + }, c1); + break; + } + case 10: { + // division + uint64_t* name1 = reinterpret_cast(cmd + 4); + uint64_t* name2 = reinterpret_cast(cmd + 12); + aum_array_t& c1 = lookup(*name1); + aum_array_t& c2 = lookup(*name2); + std::visit( + [&](auto& x, auto& y) { + using T = std::decay_t; + using V = std::decay_t; + aum_array_t res; + if constexpr(std::is_same_v && std::is_same_v) + res = x / y; + else + CmiAbort("Operation not permitted"); + insert(res_name, res); + }, c1, c2); + break; + } default: { CmiAbort("Operation not implemented"); } @@ -189,7 +291,7 @@ class Main : public CBase_Main } else { - res = aum::vector(*size); + res = aum::vector(*size, aum::random{}); } insert(res_name, res); break; @@ -206,7 +308,7 @@ class Main : public CBase_Main } else { - res = aum::matrix(*size1, *size2); + res = aum::matrix(*size1, *size2, aum::random{}); } insert(res_name, res); break; @@ -255,8 +357,7 @@ class Main : public CBase_Main void register_handlers() { - CcsRegisterHandler("aum_binop", (CmiHandler) binop_handler); - CcsRegisterHandler("aum_monop", (CmiHandler) monop_handler); + CcsRegisterHandler("aum_operation", (CmiHandler) operation_handler); CcsRegisterHandler("aum_creation", (CmiHandler) creation_handler); CcsRegisterHandler("aum_fetch", (CmiHandler) fetch_handler); CcsRegisterHandler("aum_delete", (CmiHandler) delete_handler); From 8b27c3cb83f00397fe8d115cde9f5b792c1d57fb Mon Sep 17 00:00:00 2001 From: Aditya Bhosale Date: Thu, 5 May 2022 13:41:48 -0500 Subject: [PATCH 08/16] Make ccs messages asynchronous --- examples/conjugate_gradient.py | 4 +- pyproject/array.py | 55 ++++---- pyproject/ccs.py | 44 ++++-- pyproject/linalg.py | 7 +- setup.py | 2 +- {pyproject => src}/Makefile | 2 +- {pyproject => src}/server.ci | 0 {pyproject => src}/server.cpp | 244 +++++++++++++++++++++------------ src/server.h | 29 ++++ 9 files changed, 264 insertions(+), 123 deletions(-) rename {pyproject => src}/Makefile (91%) rename {pyproject => src}/server.ci (100%) rename {pyproject => src}/server.cpp (59%) create mode 100644 src/server.h diff --git a/examples/conjugate_gradient.py b/examples/conjugate_gradient.py index a9c2f35..094ecbe 100644 --- a/examples/conjugate_gradient.py +++ b/examples/conjugate_gradient.py @@ -29,8 +29,8 @@ def solve(A, b): if __name__ == '__main__': connect("172.17.0.1", 10000) - A = ndarray(2, (100, 100), np.float64, init_value=1.) - b = ndarray(1, 100, np.float64, init_value=1.) + A = ndarray(2, (100, 100), np.float64) + b = ndarray(1, 100, np.float64) x = solve(A, b) diff --git a/pyproject/array.py b/pyproject/array.py index b4a59de..8dee659 100644 --- a/pyproject/array.py +++ b/pyproject/array.py @@ -2,7 +2,7 @@ import numpy as np from pyproject.ccs import to_bytes, from_bytes, send_command_raw, send_command, \ send_command_async, connect, get_creation_command, get_operation_command, \ - get_fetch_command, Handlers + get_name, get_epoch, get_fetch_command, Handlers def create_ndarray(name, ndim, shape, dtype): return ndarray(ndim, shape, dtype, name=name) @@ -30,11 +30,15 @@ def __init__(self, ndim, shape, dtype, init_value=None, name=None): if name: self.name = name else: - cmd = get_creation_command(self) - self.name = send_command(Handlers.creation_handler, cmd) + self.name = get_name() + cmd = get_creation_command(self, self.name) + send_command_async(Handlers.creation_handler, cmd) def __del__(self): - cmd = to_bytes(self.name, 'l') + msg_size = 20 + cmd = to_bytes(get_epoch(), 'l') + cmd += to_bytes(msg_size, 'I') + cmd += to_bytes(self.name, 'l') send_command_async(Handlers.delete_handler, cmd) def __len__(self): @@ -53,9 +57,10 @@ def __add__(self, other): if (self.shape != other.shape).any(): raise RuntimeError("Shape mismatch %s and %s" % (self.shape, other.shape)) - cmd = get_operation_command("+", [self, other]) - res = send_command(Handlers.operation_handler, cmd) - return create_ndarray(res, self.ndim, self.shape, self.dtype) + name = get_name() + cmd = get_operation_command("+", name, [self, other]) + send_command_async(Handlers.operation_handler, cmd) + return create_ndarray(name, self.ndim, self.shape, self.dtype) def __radd__(self, other): return self + other @@ -64,9 +69,10 @@ def __sub__(self, other): if (self.shape != other.shape).any(): raise RuntimeError("Shape mismatch %s and %s" % (self.shape, other.shape)) - cmd = get_operation_command("-", [self, other]) - res = send_command(Handlers.operation_handler, cmd) - return create_ndarray(res, self.ndim, self.shape, self.dtype) + name = get_name() + cmd = get_operation_command("-", name, [self, other]) + send_command_async(Handlers.operation_handler, cmd) + return create_ndarray(name, self.ndim, self.shape, self.dtype) def __rsub__(self, other): return -1 * (self - other) @@ -78,9 +84,10 @@ def __mul__(self, other): op = '*' elif isinstance(other, float) or isinstance(other, int): op = '*s' - cmd = get_operation_command(op, [self, other]) - res = send_command(Handlers.operation_handler, cmd) - return create_ndarray(res, self.ndim, self.shape, self.dtype) + name = get_name() + cmd = get_operation_command(op, name, [self, other]) + send_command_async(Handlers.operation_handler, cmd) + return create_ndarray(name, self.ndim, self.shape, self.dtype) def __rmul__(self, other): return self * other @@ -92,9 +99,10 @@ def __truediv__(self, other): op = '/' elif isinstance(other, float) or isinstance(other, int): op = '/s' - cmd = get_operation_command(op, [self, other]) - res = send_command(Handlers.operation_handler, cmd) - return create_ndarray(res, self.ndim, self.shape, self.dtype) + name = get_name() + cmd = get_operation_command(op, name, [self, other]) + send_command_async(Handlers.operation_handler, cmd) + return create_ndarray(name, self.ndim, self.shape, self.dtype) def __matmul__(self, other): if self.ndim == 2 and other.ndim == 2: @@ -115,10 +123,10 @@ def __matmul__(self, other): res_shape = np.array([], dtype=np.int32) else: raise RuntimeError("Dimension mismatch") - - cmd = get_operation_command("@", [self, other]) - res = send_command(Handlers.operation_handler, cmd) - return create_ndarray(res, res_ndim, res_shape, self.dtype) + name = get_name() + cmd = get_operation_command("@", name, [self, other]) + send_command_async(Handlers.operation_handler, cmd) + return create_ndarray(name, res_ndim, res_shape, self.dtype) def get(self): cmd = get_fetch_command(self) @@ -133,7 +141,8 @@ def get(self): return np.frombuffer(data, np.dtype(self.dtype)).copy() def copy(self): - cmd = get_operation_command("copy", [self]) - res = send_command(Handlers.operation_handler, cmd) - return create_ndarray(res, self.ndim, self.shape, self.dtype) + name = get_name() + cmd = get_operation_command("copy", name, [self]) + send_command_async(Handlers.operation_handler, cmd) + return create_ndarray(name, self.ndim, self.shape, self.dtype) diff --git a/pyproject/ccs.py b/pyproject/ccs.py index 6b88d83..fd35ee9 100644 --- a/pyproject/ccs.py +++ b/pyproject/ccs.py @@ -3,10 +3,25 @@ from pyproject import array server = None +client_id = None +next_name = 0 +epoch = 0 OPCODES = {'+': 1, '-': 2, '*': 3 ,'/': 4, '@': 5, 'copy': 6, 'axpy': 7, 'axpy_multiplier': 8, '*s': 9, '/s': 10} +def get_epoch(): + global epoch + curr_epoch = epoch + epoch += 1 + return curr_epoch + +def get_name(): + global next_name + curr_name = next_name + next_name += 1 + return (client_id << 56) + curr_name + def to_bytes(value, dtype='i'): return struct.pack(dtype, value) @@ -17,22 +32,27 @@ def send_command_raw(handler, msg, reply_size): server.send_request(handler, 0, msg) return server.receive_response(reply_size) -def send_command(handler, msg, reply_size=8): - return from_bytes(send_command_raw(handler, msg, reply_size), 'l') +def send_command(handler, msg, reply_size=1, reply_type='B'): + return from_bytes(send_command_raw(handler, msg, reply_size), reply_type) def send_command_async(handler, msg): server.send_request(handler, 0, msg) def connect(server_ip, server_port): - global server + global server, client_id server = Server(server_ip, server_port) server.connect() + client_id = send_command(Handlers.connection_handler, "") -def get_creation_command(arr): +def get_creation_command(arr, name): """ Generate array creation CCS command """ - cmd = to_bytes(arr.ndim, 'i') + msg_size = 25 + arr.ndim * 8 + (8 if arr.init_value else 0) + cmd = to_bytes(get_epoch(), 'l') + cmd += to_bytes(msg_size, 'I') + cmd += to_bytes(name, 'l') + cmd += to_bytes(arr.ndim, 'I') cmd += to_bytes(arr.init_value is not None, '?') for s in arr.shape: cmd += to_bytes(int(s), 'l') @@ -44,18 +64,25 @@ def get_fetch_command(arr): """ Generate CCS command to fetch entire array data """ - cmd = to_bytes(arr.name, 'l') + msg_size = 20 + cmd = to_bytes(get_epoch(), 'l') + cmd += to_bytes(msg_size, 'I') + cmd += to_bytes(arr.name, 'l') return cmd -def get_operation_command(operation, operands): +def get_operation_command(operation, name, operands): """ Generate CCS command for operation """ if operation not in OPCODES: raise NotImplementedError("Operation %s not supported" % operation) + msg_size = 24 + 8 * len(operands) opcode = OPCODES.get(operation) - cmd = to_bytes(opcode, 'i') + cmd = to_bytes(get_epoch(), 'l') + cmd += to_bytes(msg_size, 'I') + cmd += to_bytes(name, 'l') + cmd += to_bytes(opcode, 'I') for x in operands: if isinstance(x, array.ndarray): cmd += to_bytes(x.name, 'l') @@ -64,6 +91,7 @@ def get_operation_command(operation, operands): return cmd class Handlers(object): + connection_handler = b'aum_connect' creation_handler = b'aum_creation' operation_handler = b'aum_operation' fetch_handler = b'aum_fetch' diff --git a/pyproject/linalg.py b/pyproject/linalg.py index f082f5a..935990a 100644 --- a/pyproject/linalg.py +++ b/pyproject/linalg.py @@ -2,7 +2,7 @@ import struct import numpy as np from pyccs import Server -from pyproject.ccs import send_command, get_operation_command, Handlers +from pyproject.ccs import get_name, send_command, get_operation_command, Handlers from pyproject.array import create_ndarray @@ -13,7 +13,8 @@ def axpy(a, x, y, multiplier=None): operation = 'axpy_multiplier' else: operation = 'axpy' - cmd = get_operation_command(operation, operands) - res = send_command(Handlers.operation_handler, cmd) + res = get_name() + cmd = get_operation_command(operation, res, operands) + send_command(Handlers.operation_handler, cmd) return create_ndarray(res, x.ndim, x.shape, x.dtype) diff --git a/setup.py b/setup.py index 2d33135..b521790 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ def get_version(): def compile_server(): charmc = os.environ.get('CHARMC', '~/charm/netlrts-linux-x86_64/bin/charmc') aum_base = os.environ.get('AUM_HOME', '~/LibAum') - subprocess.run(["make", "-C", "pyproject/", + subprocess.run(["make", "-C", "src/", "CHARMC=%s" % charmc, "BASE_DIR=%s" % aum_base]) diff --git a/pyproject/Makefile b/src/Makefile similarity index 91% rename from pyproject/Makefile rename to src/Makefile index 324a5f9..b84531a 100644 --- a/pyproject/Makefile +++ b/src/Makefile @@ -1,7 +1,7 @@ CHARMC=/home/adityapb/charm/charm/netlrts-linux-x86_64/bin/charmc BASE_DIR=/home/adityapb/charm/LibAum LIBS_DIR=$(BASE_DIR) -OPTS=-c++-option -std=c++17 -O3 +OPTS=-c++-option -std=c++17 -O3 -DNDEBUG all: server diff --git a/pyproject/server.ci b/src/server.ci similarity index 100% rename from pyproject/server.ci rename to src/server.ci diff --git a/pyproject/server.cpp b/src/server.cpp similarity index 59% rename from pyproject/server.cpp rename to src/server.cpp index 8177179..c7c82cc 100644 --- a/pyproject/server.cpp +++ b/src/server.cpp @@ -1,6 +1,9 @@ #include #include #include +#include +#include +#include "server.h" #include "converse.h" #include "conv-ccs.h" @@ -12,7 +15,9 @@ using aum_name_t = uint64_t; using aum_array_t = std::variant; std::unordered_map symbol_table; -aum_name_t next_name = 0; +std::priority_queue, epoch_compare> message_buffer; +uint64_t next_epoch = 0; +uint8_t next_client_id = 0; class Main : public CBase_Main { @@ -24,34 +29,75 @@ class Main : public CBase_Main inline static void insert(aum_name_t name, aum_array_t arr) { - CkPrintf("Created array %lld on server\n", name); +#ifndef NDEBUG + CkPrintf("Created array %" PRIu64 " on server\n", name); +#endif symbol_table.emplace(name, arr); } inline static void remove(aum_name_t name) { symbol_table.erase(name); +#ifndef NDEBUG + CkPrintf("Deleted array %" PRIu64 " on server\n", name); +#endif } - inline static aum_name_t get_name() + inline static uint8_t get_client_id() { - return next_name++; + if (next_client_id == 255) + CmiAbort("Too many clients connected to the server"); + return next_client_id++; } static aum_array_t& lookup(aum_name_t name) { auto find = symbol_table.find(name); if (find == std::end(symbol_table)) + { +#ifndef NDEBUG + CkPrintf("Active symbols: "); + for (auto it: symbol_table) + CkPrintf("%" PRIu64 ", ", it.first); + CkPrintf("\n"); +#endif CmiAbort("Symbol %i not found", name); + } return find->second; } + static void flush_buffer() + { + while (extract_epoch(message_buffer.top()) == next_epoch) + { + char* msg = message_buffer.top(); + message_buffer.pop(); + operation_handler(msg); + free(msg); + next_epoch++; + } + } + + static void insert_buffer(uint32_t msg_size, char* msg) + { + char* msg_copy = (char*) malloc(msg_size); + memcpy(msg_copy, msg, msg_size); + message_buffer.push(msg_copy); + flush_buffer(); + } + static void operation_handler(char* msg) { - auto res_name = get_name(); - char* cmd = msg + CmiMsgHeaderSizeBytes; - uint32_t* opcode = reinterpret_cast(cmd); - switch(*opcode) + char* cmdptr = msg + CmiMsgHeaderSizeBytes; + char** cmd = &cmdptr; + uint64_t epoch = extract(cmd); + uint32_t msg_size = CmiMsgHeaderSizeBytes + extract(cmd); + if (epoch > next_epoch) + insert_buffer(msg_size, msg); + next_epoch++; + aum_name_t res_name = extract(cmd); + uint32_t opcode = extract(cmd); + switch(opcode) { case 0: { // no op @@ -59,10 +105,10 @@ class Main : public CBase_Main } case 1: { // addition - uint64_t* name1 = reinterpret_cast(cmd + 4); - uint64_t* name2 = reinterpret_cast(cmd + 12); - aum_array_t& c1 = lookup(*name1); - aum_array_t& c2 = lookup(*name2); + aum_name_t name1 = extract(cmd); + aum_name_t name2 = extract(cmd); + aum_array_t& c1 = lookup(name1); + aum_array_t& c2 = lookup(name2); std::visit( [&](auto& x, auto& y) { using T = std::decay_t; @@ -78,10 +124,10 @@ class Main : public CBase_Main } case 2: { // subtraction - uint64_t* name1 = reinterpret_cast(cmd + 4); - uint64_t* name2 = reinterpret_cast(cmd + 12); - aum_array_t& c1 = lookup(*name1); - aum_array_t& c2 = lookup(*name2); + aum_name_t name1 = extract(cmd); + aum_name_t name2 = extract(cmd); + aum_array_t& c1 = lookup(name1); + aum_array_t& c2 = lookup(name2); std::visit( [&](auto& x, auto& y) { using T = std::decay_t; @@ -97,10 +143,10 @@ class Main : public CBase_Main } case 3: { // multiply - uint64_t* name1 = reinterpret_cast(cmd + 4); - uint64_t* name2 = reinterpret_cast(cmd + 12); - aum_array_t& c1 = lookup(*name1); - aum_array_t& c2 = lookup(*name2); + aum_name_t name1 = extract(cmd); + aum_name_t name2 = extract(cmd); + aum_array_t& c1 = lookup(name1); + aum_array_t& c2 = lookup(name2); std::visit( [&](auto& x, auto& y) { using T = std::decay_t; @@ -116,16 +162,17 @@ class Main : public CBase_Main } case 4: { // division - uint64_t* name1 = reinterpret_cast(cmd + 4); - uint64_t* name2 = reinterpret_cast(cmd + 12); - aum_array_t& c1 = lookup(*name1); - aum_array_t& c2 = lookup(*name2); + aum_name_t name1 = extract(cmd); + aum_name_t name2 = extract(cmd); + aum_array_t& c1 = lookup(name1); + aum_array_t& c2 = lookup(name2); std::visit( [&](auto& x, auto& y) { using T = std::decay_t; using V = std::decay_t; aum_array_t res; - if constexpr(std::is_same_v && std::is_same_v) + if constexpr(std::is_same_v && + std::is_same_v) res = x / y; else CmiAbort("Operation not permitted"); @@ -135,20 +182,22 @@ class Main : public CBase_Main } case 5: { // mat/vec mul - uint64_t* name1 = reinterpret_cast(cmd + 4); - uint64_t* name2 = reinterpret_cast(cmd + 12); - aum_array_t& c1 = lookup(*name1); - aum_array_t& c2 = lookup(*name2); + aum_name_t name1 = extract(cmd); + aum_name_t name2 = extract(cmd); + aum_array_t& c1 = lookup(name1); + aum_array_t& c2 = lookup(name2); std::visit( [&](auto& x, auto& y) { using T = std::decay_t; using V = std::decay_t; aum_array_t res; - if constexpr(std::is_same_v && std::is_same_v) + if constexpr(std::is_same_v && + std::is_same_v) // Matmul here CmiAbort("Matrix multiplication not yet implemented"); else if constexpr((std::is_same_v || - std::is_same_v) && std::is_same_v) + std::is_same_v) && + std::is_same_v) res = aum::dot(x, y); else CmiAbort("Operation not permitted"); @@ -158,12 +207,13 @@ class Main : public CBase_Main } // FIXME implement a matrix copy case 6: { - uint64_t* name1 = reinterpret_cast(cmd + 4); - aum_array_t& c1 = lookup(*name1); + aum_name_t name1 = extract(cmd); + aum_array_t& c1 = lookup(name1); std::visit( [&](auto& x) { using T = std::decay_t; - if constexpr(std::is_same_v || std::is_same_v) + if constexpr(std::is_same_v || + std::is_same_v) { aum_array_t res = aum::copy(x); insert(res_name, res); @@ -177,19 +227,20 @@ class Main : public CBase_Main } case 7: { // axpy - uint64_t* name1 = reinterpret_cast(cmd + 4); - uint64_t* name2 = reinterpret_cast(cmd + 12); - uint64_t* name3 = reinterpret_cast(cmd + 20); - aum_array_t& c1 = lookup(*name1); - aum_array_t& c2 = lookup(*name2); - aum_array_t& c3 = lookup(*name3); + aum_name_t name1 = extract(cmd); + aum_name_t name2 = extract(cmd); + aum_name_t name3 = extract(cmd); + aum_array_t& c1 = lookup(name1); + aum_array_t& c2 = lookup(name2); + aum_array_t& c3 = lookup(name3); std::visit( [&](auto& a, auto& x, auto& y) { using S = std::decay_t; using T = std::decay_t; using V = std::decay_t; aum_array_t res; - if constexpr(std::is_same_v && std::is_same_v && + if constexpr(std::is_same_v && + std::is_same_v && std::is_same_v) res = aum::blas::axpy(a, x, y); else @@ -200,23 +251,23 @@ class Main : public CBase_Main } case 8: { // axpy multiplier - uint64_t* name1 = reinterpret_cast(cmd + 4); - uint64_t* name2 = reinterpret_cast(cmd + 12); - uint64_t* name3 = reinterpret_cast(cmd + 20); - double* multiplier = reinterpret_cast(cmd + 28); - CkPrintf("Multiplier = %f\n", *multiplier); - aum_array_t& c1 = lookup(*name1); - aum_array_t& c2 = lookup(*name2); - aum_array_t& c3 = lookup(*name3); + aum_name_t name1 = extract(cmd); + aum_name_t name2 = extract(cmd); + aum_name_t name3 = extract(cmd); + double multiplier = extract(cmd); + aum_array_t& c1 = lookup(name1); + aum_array_t& c2 = lookup(name2); + aum_array_t& c3 = lookup(name3); std::visit( [&](auto& a, auto& x, auto& y) { using S = std::decay_t; using T = std::decay_t; using V = std::decay_t; aum_array_t res; - if constexpr(std::is_same_v && std::is_same_v && + if constexpr(std::is_same_v && + std::is_same_v && std::is_same_v) - res = aum::blas::axpy(*multiplier, a, x, y); + res = aum::blas::axpy(multiplier, a, x, y); else CmiAbort("Operation not permitted"); insert(res_name, res); @@ -225,30 +276,31 @@ class Main : public CBase_Main } case 9: { // multiply - uint64_t* name1 = reinterpret_cast(cmd + 4); - double* y = reinterpret_cast(cmd + 12); - aum_array_t& c1 = lookup(*name1); + aum_name_t name1 = extract(cmd); + double y = extract(cmd); + aum_array_t& c1 = lookup(name1); std::visit( [&](auto& x) { using T = std::decay_t; aum_array_t res; - res = *y * x; + res = y * x; insert(res_name, res); }, c1); break; } case 10: { // division - uint64_t* name1 = reinterpret_cast(cmd + 4); - uint64_t* name2 = reinterpret_cast(cmd + 12); - aum_array_t& c1 = lookup(*name1); - aum_array_t& c2 = lookup(*name2); + aum_name_t name1 = extract(cmd); + aum_name_t name2 = extract(cmd); + aum_array_t& c1 = lookup(name1); + aum_array_t& c2 = lookup(name2); std::visit( [&](auto& x, auto& y) { using T = std::decay_t; using V = std::decay_t; aum_array_t res; - if constexpr(std::is_same_v && std::is_same_v) + if constexpr(std::is_same_v && + std::is_same_v) res = x / y; else CmiAbort("Operation not permitted"); @@ -260,7 +312,6 @@ class Main : public CBase_Main CmiAbort("Operation not implemented"); } } - CcsSendReply(NAME_SIZE, (void*) &res_name); } static void creation_handler(char* msg) @@ -270,11 +321,17 @@ class Main : public CBase_Main * in each dimension */ // FIXME need a field for dtype - auto res_name = get_name(); - char* cmd = msg + CmiMsgHeaderSizeBytes; - uint32_t* ndim = reinterpret_cast(cmd); - bool* has_init = reinterpret_cast(cmd + 4); - switch(*ndim) + char* cmdptr = msg + CmiMsgHeaderSizeBytes; + char** cmd = &cmdptr; + uint64_t epoch = extract(cmd); + uint32_t msg_size = CmiMsgHeaderSizeBytes + extract(cmd); + if (epoch > next_epoch) + insert_buffer(msg_size, msg); + next_epoch++; + aum_name_t res_name = extract(cmd); + uint32_t ndim = extract(cmd); + bool has_init = extract(cmd); + switch(ndim) { case 0: { // create scalar @@ -282,33 +339,33 @@ class Main : public CBase_Main } case 1: { // create vector - uint64_t* size = reinterpret_cast(cmd + 5); + uint64_t size = extract(cmd); aum_array_t res; - if (*has_init) + if (has_init) { - double* init_value = reinterpret_cast(cmd + 13); - res = aum::vector(*size, *init_value); + double init_value = extract(cmd); + res = aum::vector(size, init_value); } else { - res = aum::vector(*size, aum::random{}); + res = aum::vector(size, aum::random{}); } insert(res_name, res); break; } case 2: { // create matrix - uint64_t* size1 = reinterpret_cast(cmd + 5); - uint64_t* size2 = reinterpret_cast(cmd + 13); + uint64_t size1 = extract(cmd); + uint64_t size2 = extract(cmd); aum_array_t res; - if (*has_init) + if (has_init) { - double* init_value = reinterpret_cast(cmd + 21); - res = aum::matrix(*size1, *size2, *init_value); + double init_value = extract(cmd); + res = aum::matrix(size1, size2, init_value); } else { - res = aum::matrix(*size1, *size2, aum::random{}); + res = aum::matrix(size1, size2, aum::random{}); } insert(res_name, res); break; @@ -317,14 +374,19 @@ class Main : public CBase_Main CmiAbort("Greater than 2 dimensions not supported"); } } - CcsSendReply(NAME_SIZE, (void*) &res_name); } static void fetch_handler(char* msg) { - char* cmd = msg + CmiMsgHeaderSizeBytes; - aum_name_t* name = reinterpret_cast(cmd); - aum_array_t& arr = lookup(*name); + char* cmdptr = msg + CmiMsgHeaderSizeBytes; + char** cmd = &cmdptr; + uint64_t epoch = extract(cmd); + uint32_t msg_size = CmiMsgHeaderSizeBytes + extract(cmd); + if (epoch > next_epoch) + insert_buffer(msg_size, msg); + next_epoch++; + aum_name_t name = extract(cmd); + aum_array_t& arr = lookup(name); void* reply = nullptr; int reply_size = 0; std::visit( @@ -344,10 +406,21 @@ class Main : public CBase_Main static void delete_handler(char* msg) { - char* cmd = msg + CmiMsgHeaderSizeBytes; - uint64_t* name = reinterpret_cast(cmd); - remove(*name); - CkPrintf("Deleted array %lld on server\n", *name); + char* cmdptr = msg + CmiMsgHeaderSizeBytes; + char** cmd = &cmdptr; + uint64_t epoch = extract(cmd); + uint32_t msg_size = CmiMsgHeaderSizeBytes + extract(cmd); + if (epoch > next_epoch) + insert_buffer(msg_size, msg); + next_epoch++; + aum_name_t name = extract(cmd); + remove(name); + } + + static void connection_handler(char* msg) + { + uint8_t client_id = get_client_id(); + CcsSendReply(1, (void*) &client_id); } inline static void exit_server(char* msg) @@ -357,6 +430,7 @@ class Main : public CBase_Main void register_handlers() { + CcsRegisterHandler("aum_connect", (CmiHandler) connection_handler); CcsRegisterHandler("aum_operation", (CmiHandler) operation_handler); CcsRegisterHandler("aum_creation", (CmiHandler) creation_handler); CcsRegisterHandler("aum_fetch", (CmiHandler) fetch_handler); diff --git a/src/server.h b/src/server.h new file mode 100644 index 0000000..dc8e7e6 --- /dev/null +++ b/src/server.h @@ -0,0 +1,29 @@ +#include + +template +inline T extract(char** msgptr, bool increment=true) +{ + T arg = *(reinterpret_cast(*msgptr)); + if (increment) + *msgptr += sizeof(T); + return arg; +} + +inline uint64_t extract_epoch(char* msg) +{ + char* cmdptr = msg + CmiMsgHeaderSizeBytes; + char** cmd = &cmdptr; + return extract(cmd); +} + +class epoch_compare +{ +public: + bool operator()(char* msg1, char* msg2) + { + uint64_t epoch1 = extract_epoch(msg1); + uint64_t epoch2 = extract_epoch(msg2); + return epoch1 < epoch2; + } +}; + From 407aa08d70fdadd727c9e3c22f68a36ffd6ed385 Mon Sep 17 00:00:00 2001 From: Aditya Bhosale Date: Thu, 5 May 2022 20:31:30 -0500 Subject: [PATCH 09/16] Reuse client IDs --- pyproject/ccs.py | 8 ++++++++ src/server.cpp | 38 +++++++++++++++++++++++++------------- src/server.h | 9 ++++----- 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/pyproject/ccs.py b/pyproject/ccs.py index fd35ee9..fae4958 100644 --- a/pyproject/ccs.py +++ b/pyproject/ccs.py @@ -1,4 +1,5 @@ import struct +import atexit from pyccs import Server from pyproject import array @@ -43,6 +44,12 @@ def connect(server_ip, server_port): server = Server(server_ip, server_port) server.connect() client_id = send_command(Handlers.connection_handler, "") + atexit.register(disconnect) + +def disconnect(): + global client_id + cmd = to_bytes(client_id, 'I') + send_command_async(Handlers.disconnection_handler, cmd) def get_creation_command(arr, name): """ @@ -92,6 +99,7 @@ def get_operation_command(operation, name, operands): class Handlers(object): connection_handler = b'aum_connect' + disconnection_handler = b'aum_disconnect' creation_handler = b'aum_creation' operation_handler = b'aum_operation' fetch_handler = b'aum_fetch' diff --git a/src/server.cpp b/src/server.cpp index c7c82cc..6177305 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -9,22 +9,25 @@ #include "server.decl.h" -#define NAME_SIZE 8 - using aum_name_t = uint64_t; using aum_array_t = std::variant; std::unordered_map symbol_table; std::priority_queue, epoch_compare> message_buffer; +std::queue client_ids; uint64_t next_epoch = 0; -uint8_t next_client_id = 0; class Main : public CBase_Main { public: Main(CkArgMsg* msg) { + for (uint16_t i = 0; i < 256; i++) + client_ids.push((uint8_t) i); register_handlers(); +#ifndef NDEBUG + CkPrintf("Initialization done\n"); +#endif } inline static void insert(aum_name_t name, aum_array_t arr) @@ -45,9 +48,11 @@ class Main : public CBase_Main inline static uint8_t get_client_id() { - if (next_client_id == 255) + if (client_ids.empty()) CmiAbort("Too many clients connected to the server"); - return next_client_id++; + uint8_t client_id = client_ids.front(); + client_ids.pop(); + return client_id; } static aum_array_t& lookup(aum_name_t name) @@ -88,8 +93,7 @@ class Main : public CBase_Main static void operation_handler(char* msg) { - char* cmdptr = msg + CmiMsgHeaderSizeBytes; - char** cmd = &cmdptr; + char* cmd = msg + CmiMsgHeaderSizeBytes; uint64_t epoch = extract(cmd); uint32_t msg_size = CmiMsgHeaderSizeBytes + extract(cmd); if (epoch > next_epoch) @@ -321,8 +325,7 @@ class Main : public CBase_Main * in each dimension */ // FIXME need a field for dtype - char* cmdptr = msg + CmiMsgHeaderSizeBytes; - char** cmd = &cmdptr; + char* cmd = msg + CmiMsgHeaderSizeBytes; uint64_t epoch = extract(cmd); uint32_t msg_size = CmiMsgHeaderSizeBytes + extract(cmd); if (epoch > next_epoch) @@ -378,8 +381,7 @@ class Main : public CBase_Main static void fetch_handler(char* msg) { - char* cmdptr = msg + CmiMsgHeaderSizeBytes; - char** cmd = &cmdptr; + char* cmd = msg + CmiMsgHeaderSizeBytes; uint64_t epoch = extract(cmd); uint32_t msg_size = CmiMsgHeaderSizeBytes + extract(cmd); if (epoch > next_epoch) @@ -406,8 +408,7 @@ class Main : public CBase_Main static void delete_handler(char* msg) { - char* cmdptr = msg + CmiMsgHeaderSizeBytes; - char** cmd = &cmdptr; + char* cmd = msg + CmiMsgHeaderSizeBytes; uint64_t epoch = extract(cmd); uint32_t msg_size = CmiMsgHeaderSizeBytes + extract(cmd); if (epoch > next_epoch) @@ -423,6 +424,16 @@ class Main : public CBase_Main CcsSendReply(1, (void*) &client_id); } + static void disconnection_handler(char* msg) + { + char* cmd = msg + CmiMsgHeaderSizeBytes; + uint8_t client_id = extract(cmd); + client_ids.push(client_id); +#ifndef NDEBUG + CkPrintf("Disconnected %" PRIu8 " from server\n", client_id); +#endif + } + inline static void exit_server(char* msg) { CkExit(); @@ -431,6 +442,7 @@ class Main : public CBase_Main void register_handlers() { CcsRegisterHandler("aum_connect", (CmiHandler) connection_handler); + CcsRegisterHandler("aum_disconnect", (CmiHandler) disconnection_handler); CcsRegisterHandler("aum_operation", (CmiHandler) operation_handler); CcsRegisterHandler("aum_creation", (CmiHandler) creation_handler); CcsRegisterHandler("aum_fetch", (CmiHandler) fetch_handler); diff --git a/src/server.h b/src/server.h index dc8e7e6..3e02f94 100644 --- a/src/server.h +++ b/src/server.h @@ -1,18 +1,17 @@ #include template -inline T extract(char** msgptr, bool increment=true) +inline T extract(char* &msg, bool increment=true) { - T arg = *(reinterpret_cast(*msgptr)); + T arg = *(reinterpret_cast(msg)); if (increment) - *msgptr += sizeof(T); + msg += sizeof(T); return arg; } inline uint64_t extract_epoch(char* msg) { - char* cmdptr = msg + CmiMsgHeaderSizeBytes; - char** cmd = &cmdptr; + char* cmd = msg + CmiMsgHeaderSizeBytes; return extract(cmd); } From 27cb3cb83523a57ce251e4d325f79326db1c7d0f Mon Sep 17 00:00:00 2001 From: Aditya Bhosale Date: Sat, 14 May 2022 16:28:31 -0500 Subject: [PATCH 10/16] Fix epochs with multiple clients --- pyproject/array.py | 9 ++++++--- pyproject/ccs.py | 34 ++++++++++++++++++++-------------- src/server.cpp | 45 +++++++++++++++++++++++++++++++-------------- 3 files changed, 57 insertions(+), 31 deletions(-) diff --git a/pyproject/array.py b/pyproject/array.py index 8dee659..271afde 100644 --- a/pyproject/array.py +++ b/pyproject/array.py @@ -35,10 +35,13 @@ def __init__(self, ndim, shape, dtype, init_value=None, name=None): send_command_async(Handlers.creation_handler, cmd) def __del__(self): - msg_size = 20 - cmd = to_bytes(get_epoch(), 'l') + from pyproject.ccs import client_id + global client_id + msg_size = 21 + cmd = to_bytes(client_id, 'B') + cmd += to_bytes(get_epoch(), 'L') cmd += to_bytes(msg_size, 'I') - cmd += to_bytes(self.name, 'l') + cmd += to_bytes(self.name, 'L') send_command_async(Handlers.delete_handler, cmd) def __len__(self): diff --git a/pyproject/ccs.py b/pyproject/ccs.py index fae4958..43e9aed 100644 --- a/pyproject/ccs.py +++ b/pyproject/ccs.py @@ -23,10 +23,10 @@ def get_name(): next_name += 1 return (client_id << 56) + curr_name -def to_bytes(value, dtype='i'): +def to_bytes(value, dtype='I'): return struct.pack(dtype, value) -def from_bytes(bvalue, dtype='i'): +def from_bytes(bvalue, dtype='I'): return struct.unpack(dtype, bvalue)[0] def send_command_raw(handler, msg, reply_size): @@ -48,21 +48,23 @@ def connect(server_ip, server_port): def disconnect(): global client_id - cmd = to_bytes(client_id, 'I') + cmd = to_bytes(client_id, 'B') send_command_async(Handlers.disconnection_handler, cmd) def get_creation_command(arr, name): """ Generate array creation CCS command """ - msg_size = 25 + arr.ndim * 8 + (8 if arr.init_value else 0) - cmd = to_bytes(get_epoch(), 'l') + global client_id + msg_size = 26 + arr.ndim * 8 + (8 if arr.init_value else 0) + cmd = to_bytes(client_id, 'B') + cmd += to_bytes(get_epoch(), 'L') cmd += to_bytes(msg_size, 'I') - cmd += to_bytes(name, 'l') + cmd += to_bytes(name, 'L') cmd += to_bytes(arr.ndim, 'I') cmd += to_bytes(arr.init_value is not None, '?') for s in arr.shape: - cmd += to_bytes(int(s), 'l') + cmd += to_bytes(int(s), 'L') if arr.init_value is not None: cmd += to_bytes(arr.init_value, 'd') return cmd @@ -71,28 +73,32 @@ def get_fetch_command(arr): """ Generate CCS command to fetch entire array data """ - msg_size = 20 - cmd = to_bytes(get_epoch(), 'l') + global client_id + msg_size = 21 + cmd = to_bytes(client_id, 'B') + cmd += to_bytes(get_epoch(), 'L') cmd += to_bytes(msg_size, 'I') - cmd += to_bytes(arr.name, 'l') + cmd += to_bytes(arr.name, 'L') return cmd def get_operation_command(operation, name, operands): """ Generate CCS command for operation """ + global client_id if operation not in OPCODES: raise NotImplementedError("Operation %s not supported" % operation) - msg_size = 24 + 8 * len(operands) + msg_size = 25 + 8 * len(operands) opcode = OPCODES.get(operation) - cmd = to_bytes(get_epoch(), 'l') + cmd = to_bytes(client_id, 'B') + cmd += to_bytes(get_epoch(), 'L') cmd += to_bytes(msg_size, 'I') - cmd += to_bytes(name, 'l') + cmd += to_bytes(name, 'L') cmd += to_bytes(opcode, 'I') for x in operands: if isinstance(x, array.ndarray): - cmd += to_bytes(x.name, 'l') + cmd += to_bytes(x.name, 'L') elif isinstance(x, float) or isinstance(x, int): cmd += to_bytes(x, 'd') return cmd diff --git a/src/server.cpp b/src/server.cpp index 6177305..dd28c64 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include "server.h" #include "converse.h" @@ -14,16 +15,17 @@ using aum_array_t = std::variant; std::unordered_map symbol_table; std::priority_queue, epoch_compare> message_buffer; -std::queue client_ids; -uint64_t next_epoch = 0; +std::stack client_ids; +uint64_t next_epoch[256]; class Main : public CBase_Main { public: Main(CkArgMsg* msg) { - for (uint16_t i = 0; i < 256; i++) + for (int16_t i = 255; i >= 0; i--) client_ids.push((uint8_t) i); + memset(next_epoch, 0, 256 * sizeof(uint64_t)); register_handlers(); #ifndef NDEBUG CkPrintf("Initialization done\n"); @@ -50,7 +52,7 @@ class Main : public CBase_Main { if (client_ids.empty()) CmiAbort("Too many clients connected to the server"); - uint8_t client_id = client_ids.front(); + uint8_t client_id = client_ids.top(); client_ids.pop(); return client_id; } @@ -73,18 +75,26 @@ class Main : public CBase_Main static void flush_buffer() { - while (extract_epoch(message_buffer.top()) == next_epoch) + while (true) { char* msg = message_buffer.top(); + char* cmd = msg + CmiMsgHeaderSizeBytes; + uint8_t client_id = extract(cmd); + uint64_t epoch = extract(cmd); + if (epoch != next_epoch[client_id]) + break; message_buffer.pop(); operation_handler(msg); free(msg); - next_epoch++; + next_epoch[client_id]++; } } static void insert_buffer(uint32_t msg_size, char* msg) { +#ifndef NDEBUG + CkPrintf("Inserting into buffer\n"); +#endif char* msg_copy = (char*) malloc(msg_size); memcpy(msg_copy, msg, msg_size); message_buffer.push(msg_copy); @@ -94,13 +104,17 @@ class Main : public CBase_Main static void operation_handler(char* msg) { char* cmd = msg + CmiMsgHeaderSizeBytes; + uint8_t client_id = extract(cmd); uint64_t epoch = extract(cmd); uint32_t msg_size = CmiMsgHeaderSizeBytes + extract(cmd); - if (epoch > next_epoch) + if (epoch > next_epoch[client_id]) insert_buffer(msg_size, msg); - next_epoch++; + next_epoch[client_id]++; aum_name_t res_name = extract(cmd); uint32_t opcode = extract(cmd); +#ifndef NDEBUG + CkPrintf("Operation %" PRIu32 "\n", opcode); +#endif switch(opcode) { case 0: { @@ -326,11 +340,12 @@ class Main : public CBase_Main */ // FIXME need a field for dtype char* cmd = msg + CmiMsgHeaderSizeBytes; + uint8_t client_id = extract(cmd); uint64_t epoch = extract(cmd); uint32_t msg_size = CmiMsgHeaderSizeBytes + extract(cmd); - if (epoch > next_epoch) + if (epoch > next_epoch[client_id]) insert_buffer(msg_size, msg); - next_epoch++; + next_epoch[client_id]++; aum_name_t res_name = extract(cmd); uint32_t ndim = extract(cmd); bool has_init = extract(cmd); @@ -382,11 +397,12 @@ class Main : public CBase_Main static void fetch_handler(char* msg) { char* cmd = msg + CmiMsgHeaderSizeBytes; + uint8_t client_id = extract(cmd); uint64_t epoch = extract(cmd); uint32_t msg_size = CmiMsgHeaderSizeBytes + extract(cmd); - if (epoch > next_epoch) + if (epoch > next_epoch[client_id]) insert_buffer(msg_size, msg); - next_epoch++; + next_epoch[client_id]++; aum_name_t name = extract(cmd); aum_array_t& arr = lookup(name); void* reply = nullptr; @@ -409,11 +425,12 @@ class Main : public CBase_Main static void delete_handler(char* msg) { char* cmd = msg + CmiMsgHeaderSizeBytes; + uint8_t client_id = extract(cmd); uint64_t epoch = extract(cmd); uint32_t msg_size = CmiMsgHeaderSizeBytes + extract(cmd); - if (epoch > next_epoch) + if (epoch > next_epoch[client_id]) insert_buffer(msg_size, msg); - next_epoch++; + next_epoch[client_id]++; aum_name_t name = extract(cmd); remove(name); } From 18c866aabbcdfd9bb8e579c36db6443ac2d8673c Mon Sep 17 00:00:00 2001 From: Aditya Bhosale Date: Thu, 26 May 2022 22:28:19 -0500 Subject: [PATCH 11/16] Add code for lazy evaluation --- examples/graph.py | 19 ++ examples/simple.py | 18 +- pyproject/__init__.py | 1 + pyproject/array.py | 163 +++++++++------ pyproject/ast.py | 76 +++++++ pyproject/ccs.py | 50 +---- pyproject/linalg.py | 9 +- src/Makefile | 2 +- src/ast.hpp | 83 ++++++++ src/server.cpp | 456 +----------------------------------------- src/server.h | 28 --- src/server.hpp | 299 +++++++++++++++++++++++++++ 12 files changed, 613 insertions(+), 591 deletions(-) create mode 100644 examples/graph.py create mode 100644 pyproject/__init__.py create mode 100644 pyproject/ast.py create mode 100644 src/ast.hpp delete mode 100644 src/server.h create mode 100644 src/server.hpp diff --git a/examples/graph.py b/examples/graph.py new file mode 100644 index 0000000..dc6d91a --- /dev/null +++ b/examples/graph.py @@ -0,0 +1,19 @@ +from pyproject.array import connect, ndarray +import pyproject.linalg as lg +import numpy as np + +def f(): + v = ndarray(1, 10, np.float64) + b = ndarray(1, 10, np.float64) + c = ndarray(1, 10, np.float64) + y = v + b + c + z = v - y + z.evaluate() + print(z.shape) + + +if __name__ == '__main__': + connect("172.17.0.1", 10000) + s = f() + + diff --git a/examples/simple.py b/examples/simple.py index 1115e5f..2fc983a 100644 --- a/examples/simple.py +++ b/examples/simple.py @@ -3,14 +3,16 @@ import numpy as np def f(): - x = ndarray(1, 10, np.float64, init_value=1.) - y = ndarray(1, 10, np.float64, init_value=1.) - - a = ndarray(1, 10, np.float64, init_value=1.) - b = ndarray(1, 10, np.float64, init_value=1.) - - a = x @ y - r = lg.axpy(a, x, y, multiplier=-1.) + vnp = np.array([2, 1, 1], dtype=np.float64) + Anp = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float64) + bnp = np.array([1, 0], dtype=np.float64) + v = ndarray(1, 3, np.float64, nparr=vnp) + b = ndarray(1, 2, np.float64, nparr=bnp) + A = ndarray(2, (2, 3), np.float64, nparr=Anp) + Av = A @ v + Avnorm = Av @ b + print(Avnorm.get()) + #print("Actual =", xnp @ ynp) if __name__ == '__main__': diff --git a/pyproject/__init__.py b/pyproject/__init__.py new file mode 100644 index 0000000..a1c1976 --- /dev/null +++ b/pyproject/__init__.py @@ -0,0 +1 @@ +__version__ = '0.1.dev' diff --git a/pyproject/array.py b/pyproject/array.py index 271afde..e924964 100644 --- a/pyproject/array.py +++ b/pyproject/array.py @@ -1,14 +1,19 @@ import sys import numpy as np +from pyproject.ast import ASTNode from pyproject.ccs import to_bytes, from_bytes, send_command_raw, send_command, \ - send_command_async, connect, get_creation_command, get_operation_command, \ - get_name, get_epoch, get_fetch_command, Handlers + send_command_async, connect, get_creation_command, \ + get_name, get_fetch_command, Handlers, OPCODES + + +def create_ndarray(ndim, dtype, shape=None, name=None, command_buffer=None): + return ndarray(ndim, dtype=dtype, shape=shape, name=name, + command_buffer=command_buffer) -def create_ndarray(name, ndim, shape, dtype): - return ndarray(ndim, shape, dtype, name=name) class ndarray: - def __init__(self, ndim, shape, dtype, init_value=None, name=None): + def __init__(self, ndim, shape=None, dtype=np.float64, init_value=None, + nparr=None, name=None, command_buffer=None): """ This is the wrapper class for AUM array objects. The argument 'name' should be None except when wrapping @@ -21,28 +26,45 @@ def __init__(self, ndim, shape, dtype, init_value=None, name=None): self.ndim = ndim self.itemsize = np.dtype(dtype).itemsize self.init_value = init_value + self.command_buffer = command_buffer if isinstance(shape, np.ndarray) or isinstance(shape, list) or \ isinstance(shape, tuple): - self.shape = np.asarray(shape, dtype=np.int32) + self._shape = np.asarray(shape, dtype=np.int32) + elif shape is not None: + self._shape = np.asarray([shape], dtype=np.int32) else: - self.shape = np.asarray([shape], dtype=np.int32) - self.size = np.prod(self.shape) - if name: - self.name = name + self._shape = np.zeros(self.ndim, dtype=np.int32) + self.valid = False + if command_buffer is None: + self.valid = True + if name: + self.name = name + self.command_buffer = ASTNode(self.name, 0, [self]) + else: + self.name = get_name() + if nparr is not None: + buf = nparr.tobytes() + else: + buf = None + cmd = get_creation_command(self, self.name, buf=buf) + if not send_command(Handlers.creation_handler, cmd, + reply_type='?'): + raise RuntimeError("Error creating array on server") + self.command_buffer = ASTNode(self.name, 0, [self]) else: - self.name = get_name() - cmd = get_creation_command(self, self.name) - send_command_async(Handlers.creation_handler, cmd) + self.name = name + + @property + def shape(self): + self._flush_command_buffer() + return self._shape def __del__(self): from pyproject.ccs import client_id global client_id - msg_size = 21 - cmd = to_bytes(client_id, 'B') - cmd += to_bytes(get_epoch(), 'L') - cmd += to_bytes(msg_size, 'I') - cmd += to_bytes(self.name, 'L') - send_command_async(Handlers.delete_handler, cmd) + if self.valid: + cmd = to_bytes(self.name, 'L') + send_command_async(Handlers.delete_handler, cmd) def __len__(self): return self.shape[0] @@ -50,32 +72,28 @@ def __len__(self): def __str__(self): pass - def __repr__(self): - pass + #def __repr__(self): + # #self._flush_command_buffer() + # # FIXME add repr + # pass def __neg__(self): return self * -1 def __add__(self, other): - if (self.shape != other.shape).any(): - raise RuntimeError("Shape mismatch %s and %s" % (self.shape, - other.shape)) - name = get_name() - cmd = get_operation_command("+", name, [self, other]) - send_command_async(Handlers.operation_handler, cmd) - return create_ndarray(name, self.ndim, self.shape, self.dtype) + res = get_name() + cmd_buffer = ASTNode(res, OPCODES.get('+'), [self, other]) + return create_ndarray(self.ndim, self.dtype, + name=res, command_buffer=cmd_buffer) def __radd__(self, other): return self + other def __sub__(self, other): - if (self.shape != other.shape).any(): - raise RuntimeError("Shape mismatch %s and %s" % (self.shape, - other.shape)) - name = get_name() - cmd = get_operation_command("-", name, [self, other]) - send_command_async(Handlers.operation_handler, cmd) - return create_ndarray(name, self.ndim, self.shape, self.dtype) + res = get_name() + cmd_buffer = ASTNode(res, OPCODES.get('-'), [self, other]) + return create_ndarray(self.ndim, self.dtype, + name=res, command_buffer=cmd_buffer) def __rsub__(self, other): return -1 * (self - other) @@ -87,10 +105,10 @@ def __mul__(self, other): op = '*' elif isinstance(other, float) or isinstance(other, int): op = '*s' - name = get_name() - cmd = get_operation_command(op, name, [self, other]) - send_command_async(Handlers.operation_handler, cmd) - return create_ndarray(name, self.ndim, self.shape, self.dtype) + res = get_name() + cmd_buffer = ASTNode(res, OPCODES.get(op), [self, other]) + return create_ndarray(self.ndim, self.dtype, + name=res, command_buffer=cmd_buffer) def __rmul__(self, other): return self * other @@ -102,36 +120,51 @@ def __truediv__(self, other): op = '/' elif isinstance(other, float) or isinstance(other, int): op = '/s' - name = get_name() - cmd = get_operation_command(op, name, [self, other]) - send_command_async(Handlers.operation_handler, cmd) - return create_ndarray(name, self.ndim, self.shape, self.dtype) + res = get_name() + cmd_buffer = ASTNode(res, OPCODES.get(op), [self, other]) + return create_ndarray(self.ndim, self.dtype, + name=res, command_buffer=cmd_buffer) def __matmul__(self, other): if self.ndim == 2 and other.ndim == 2: res_ndim = 2 - if self.shape[1] != other.shape[0]: - raise RuntimeError("Shape mismatch") - res_shape = np.array([self.shape[0], other.shape[1]], - dtype=np.int32) elif self.ndim == 2 and other.ndim == 1: res_ndim = 1 - if self.shape[1] != other.shape[0]: - raise RuntimeError("Shape mismatch") - res_shape = np.array([self.shape[0]], dtype=np.int32) elif self.ndim == 1 and other.ndim == 1: res_ndim = 0 - if self.shape[0] != other.shape[0]: - raise RuntimeError("Shape mismatch") - res_shape = np.array([], dtype=np.int32) else: raise RuntimeError("Dimension mismatch") - name = get_name() - cmd = get_operation_command("@", name, [self, other]) - send_command_async(Handlers.operation_handler, cmd) - return create_ndarray(name, res_ndim, res_shape, self.dtype) + res = get_name() + cmd_buffer = ASTNode(res, OPCODES.get('@'), [self, other]) + return create_ndarray(res_ndim, self.dtype, + name=res, command_buffer=cmd_buffer) + + def _flush_command_buffer(self): + # send the command to server + # finally set command buffer to array name + if self.valid: + return + validated_arrays = {self.name : self} + cmd = self.command_buffer.get_command(validated_arrays) + reply_size = 0 + for name, arr in validated_arrays.items(): + reply_size += 8 + 8 * arr.ndim + metadata = send_command_raw(Handlers.operation_handler, + cmd, reply_size=reply_size) + # traverse metadata + offset = 0 + for i in range(len(validated_arrays)): + name = from_bytes(metadata[offset : offset + 8], 'L') + offset += 8 + arr = validated_arrays[name] + for d in range(arr.ndim): + arr._shape[d] = from_bytes(metadata[offset : offset + 8], 'L') + offset += 8 + arr.validate() + self.validate() def get(self): + self._flush_command_buffer() cmd = get_fetch_command(self) if self.ndim == 0: total_size = self.itemsize @@ -143,9 +176,17 @@ def get(self): data = cast(memoryview, data_ptr) return np.frombuffer(data, np.dtype(self.dtype)).copy() + def evaluate(self): + self._flush_command_buffer() + + def validate(self): + self.valid = True + self.command_buffer = ASTNode(self.name, 0, [self]) + def copy(self): - name = get_name() - cmd = get_operation_command("copy", name, [self]) - send_command_async(Handlers.operation_handler, cmd) - return create_ndarray(name, self.ndim, self.shape, self.dtype) + res = get_name() + cmd_buffer = ASTNode(res, OPCODES.get('copy'), [self]) + return create_ndarray(self.ndim, self.dtype, + name=res, command_buffer=cmd_buffer) + diff --git a/pyproject/ast.py b/pyproject/ast.py new file mode 100644 index 0000000..2a0deae --- /dev/null +++ b/pyproject/ast.py @@ -0,0 +1,76 @@ +import numpy as np +import networkx as nx +import matplotlib.pyplot as plt +from ctypes import c_long +from networkx.drawing.nx_pydot import graphviz_layout +from pyproject.ccs import OPCODES, INV_OPCODES, to_bytes + + +class ASTNode(object): + def __init__(self, name, opcode, operands): + # contains opcode, operands + # operands are ndarrays + self.name = name + self.opcode = opcode + self.operands = operands + + def get_command(self, validated_arrays, save=True): + from pyproject.array import ndarray + if self.opcode == 0: + cmd = to_bytes(self.opcode, 'L') + cmd += to_bytes(self.operands[0].name, 'L') + return cmd + cmd = to_bytes(self.opcode, 'L') + to_bytes(self.name, 'L') + cmd += to_bytes(save, '?') + to_bytes(len(self.operands), 'B') + for op in self.operands: + # an operand can also be a double + if isinstance(op, ndarray): + save_op = True if c_long.from_address(id(op)).value - 2 > 0 else False + opcmd = op.command_buffer.get_command(validated_arrays, + save=save_op) + if save_op: + validated_arrays[op.name] = op + cmd += to_bytes(len(opcmd), 'I') + cmd += opcmd + elif isinstance(op, float) or isinstance(op, int): + cmd += to_bytes(8, 'I') + cmd += to_bytes(op, 'd') + return cmd + + def plot_graph(self, G=None, node_map={}, next_id=0, parent=None): + from pyproject.array import ndarray + if G is None: + G = nx.Graph() + if self.opcode == 0: + node_map[next_id] = 'a' + str(self.operands[0].name) + G.add_node(next_id) + if parent is not None: + G.add_edge(parent, next_id) + next_id += 1 + if not self.operands[0].valid: + next_id = self.operands[0].command_buffer.plot_graph( + G, node_map, next_id, next_id - 1) + return next_id + opnode = next_id + G.add_node(next_id) + if parent is not None: + G.add_edge(parent, next_id) + node_map[next_id] = INV_OPCODES.get(self.opcode, '?') + next_id += 1 + for op in self.operands: + # an operand can also be a double + if isinstance(op, ndarray): + next_id = op.command_buffer.plot_graph( + G, node_map, next_id, opnode) + elif isinstance(op, float) or isinstance(op, int): + G.add_node(next_id) + G.add_edge(opnode, next_id) + node_map[next_id] = op + next_id += 1 + if parent is None: + pos = graphviz_layout(G, prog="dot") + nx.draw(G, pos, labels=node_map) + plt.show() + return next_id + + diff --git a/pyproject/ccs.py b/pyproject/ccs.py index 43e9aed..67ef8e1 100644 --- a/pyproject/ccs.py +++ b/pyproject/ccs.py @@ -6,16 +6,11 @@ server = None client_id = None next_name = 0 -epoch = 0 OPCODES = {'+': 1, '-': 2, '*': 3 ,'/': 4, '@': 5, 'copy': 6, 'axpy': 7, 'axpy_multiplier': 8, '*s': 9, '/s': 10} -def get_epoch(): - global epoch - curr_epoch = epoch - epoch += 1 - return curr_epoch +INV_OPCODES = {v: k for k, v in OPCODES.items()} def get_name(): global next_name @@ -51,21 +46,19 @@ def disconnect(): cmd = to_bytes(client_id, 'B') send_command_async(Handlers.disconnection_handler, cmd) -def get_creation_command(arr, name): +def get_creation_command(arr, name, buf=None): """ Generate array creation CCS command """ - global client_id - msg_size = 26 + arr.ndim * 8 + (8 if arr.init_value else 0) - cmd = to_bytes(client_id, 'B') - cmd += to_bytes(get_epoch(), 'L') - cmd += to_bytes(msg_size, 'I') - cmd += to_bytes(name, 'L') + cmd = to_bytes(name, 'L') cmd += to_bytes(arr.ndim, 'I') + cmd += to_bytes(buf is not None, '?') cmd += to_bytes(arr.init_value is not None, '?') for s in arr.shape: cmd += to_bytes(int(s), 'L') - if arr.init_value is not None: + if buf is not None: + cmd += buf + elif arr.init_value is not None: cmd += to_bytes(arr.init_value, 'd') return cmd @@ -73,34 +66,7 @@ def get_fetch_command(arr): """ Generate CCS command to fetch entire array data """ - global client_id - msg_size = 21 - cmd = to_bytes(client_id, 'B') - cmd += to_bytes(get_epoch(), 'L') - cmd += to_bytes(msg_size, 'I') - cmd += to_bytes(arr.name, 'L') - return cmd - -def get_operation_command(operation, name, operands): - """ - Generate CCS command for operation - """ - global client_id - if operation not in OPCODES: - raise NotImplementedError("Operation %s not supported" - % operation) - msg_size = 25 + 8 * len(operands) - opcode = OPCODES.get(operation) - cmd = to_bytes(client_id, 'B') - cmd += to_bytes(get_epoch(), 'L') - cmd += to_bytes(msg_size, 'I') - cmd += to_bytes(name, 'L') - cmd += to_bytes(opcode, 'I') - for x in operands: - if isinstance(x, array.ndarray): - cmd += to_bytes(x.name, 'L') - elif isinstance(x, float) or isinstance(x, int): - cmd += to_bytes(x, 'd') + cmd = to_bytes(arr.name, 'L') return cmd class Handlers(object): diff --git a/pyproject/linalg.py b/pyproject/linalg.py index 935990a..456a151 100644 --- a/pyproject/linalg.py +++ b/pyproject/linalg.py @@ -2,7 +2,7 @@ import struct import numpy as np from pyccs import Server -from pyproject.ccs import get_name, send_command, get_operation_command, Handlers +from pyproject.ccs import get_name, send_command, Handlers from pyproject.array import create_ndarray @@ -14,7 +14,8 @@ def axpy(a, x, y, multiplier=None): else: operation = 'axpy' res = get_name() - cmd = get_operation_command(operation, res, operands) - send_command(Handlers.operation_handler, cmd) - return create_ndarray(res, x.ndim, x.shape, x.dtype) + cmd_buffer = ASTNode(res, OPCODES.get(operation), operands) + return create_ndarray(self.ndim, self.dtype, + name=res, command_buffer=cmd_buffer) + diff --git a/src/Makefile b/src/Makefile index b84531a..4f6206f 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1,7 +1,7 @@ CHARMC=/home/adityapb/charm/charm/netlrts-linux-x86_64/bin/charmc BASE_DIR=/home/adityapb/charm/LibAum LIBS_DIR=$(BASE_DIR) -OPTS=-c++-option -std=c++17 -O3 -DNDEBUG +OPTS=-c++-option -std=c++17 -O3 #-DNDEBUG all: server diff --git a/src/ast.hpp b/src/ast.hpp new file mode 100644 index 0000000..5e77eab --- /dev/null +++ b/src/ast.hpp @@ -0,0 +1,83 @@ +#include +#include + + +template +inline T extract(char* &msg, bool increment=true) +{ + T arg = *(reinterpret_cast(msg)); + if (increment) + msg += sizeof(T); + return arg; +} + + +enum class operation : uint64_t +{ + noop = 0, + add = 1, + sub = 2, + mul = 3, + div = 4, + matmul = 5, + copy = 6, + axpy = 7, + axpy_multiplier = 8, + mul_scalar = 9, + div_scalar = 10 +}; + + +class astnode +{ +public: + bool store; + uint64_t name; + operation oper; + std::vector operands; +}; + + +operation lookup_operation(uint64_t opcode) +{ + return static_cast(opcode); +} + + +astnode* decode(char* cmd) +{ + uint64_t opcode = extract(cmd); + astnode* node = new astnode; + node->oper = lookup_operation(opcode); + node->name = extract(cmd); + if (opcode == 0) + return node; + + node->store = extract(cmd); + uint8_t num_operands = extract(cmd); + for (uint8_t i = 0; i < num_operands; i++) + { + uint32_t operand_size = extract(cmd); + astnode* opnode = decode(cmd); + node->operands.push_back(opnode); + cmd += operand_size; + } + + return node; +} + + +void delete_ast(astnode* node) +{ + if (node->oper == operation::noop) + { + delete node; + return; + } + + for (astnode* n : node->operands) + delete_ast(n); + + delete node; +} + diff --git a/src/server.cpp b/src/server.cpp index dd28c64..0f290b6 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -1,470 +1,32 @@ #include -#include -#include -#include -#include -#include -#include "server.h" +#include "server.hpp" #include "converse.h" #include "conv-ccs.h" #include "server.decl.h" -using aum_name_t = uint64_t; -using aum_array_t = std::variant; - -std::unordered_map symbol_table; -std::priority_queue, epoch_compare> message_buffer; -std::stack client_ids; -uint64_t next_epoch[256]; class Main : public CBase_Main { public: Main(CkArgMsg* msg) { - for (int16_t i = 255; i >= 0; i--) - client_ids.push((uint8_t) i); - memset(next_epoch, 0, 256 * sizeof(uint64_t)); + Server::initialize(); register_handlers(); #ifndef NDEBUG CkPrintf("Initialization done\n"); #endif } - inline static void insert(aum_name_t name, aum_array_t arr) - { -#ifndef NDEBUG - CkPrintf("Created array %" PRIu64 " on server\n", name); -#endif - symbol_table.emplace(name, arr); - } - - inline static void remove(aum_name_t name) - { - symbol_table.erase(name); -#ifndef NDEBUG - CkPrintf("Deleted array %" PRIu64 " on server\n", name); -#endif - } - - inline static uint8_t get_client_id() - { - if (client_ids.empty()) - CmiAbort("Too many clients connected to the server"); - uint8_t client_id = client_ids.top(); - client_ids.pop(); - return client_id; - } - - static aum_array_t& lookup(aum_name_t name) - { - auto find = symbol_table.find(name); - if (find == std::end(symbol_table)) - { -#ifndef NDEBUG - CkPrintf("Active symbols: "); - for (auto it: symbol_table) - CkPrintf("%" PRIu64 ", ", it.first); - CkPrintf("\n"); -#endif - CmiAbort("Symbol %i not found", name); - } - return find->second; - } - - static void flush_buffer() - { - while (true) - { - char* msg = message_buffer.top(); - char* cmd = msg + CmiMsgHeaderSizeBytes; - uint8_t client_id = extract(cmd); - uint64_t epoch = extract(cmd); - if (epoch != next_epoch[client_id]) - break; - message_buffer.pop(); - operation_handler(msg); - free(msg); - next_epoch[client_id]++; - } - } - - static void insert_buffer(uint32_t msg_size, char* msg) - { -#ifndef NDEBUG - CkPrintf("Inserting into buffer\n"); -#endif - char* msg_copy = (char*) malloc(msg_size); - memcpy(msg_copy, msg, msg_size); - message_buffer.push(msg_copy); - flush_buffer(); - } - - static void operation_handler(char* msg) - { - char* cmd = msg + CmiMsgHeaderSizeBytes; - uint8_t client_id = extract(cmd); - uint64_t epoch = extract(cmd); - uint32_t msg_size = CmiMsgHeaderSizeBytes + extract(cmd); - if (epoch > next_epoch[client_id]) - insert_buffer(msg_size, msg); - next_epoch[client_id]++; - aum_name_t res_name = extract(cmd); - uint32_t opcode = extract(cmd); -#ifndef NDEBUG - CkPrintf("Operation %" PRIu32 "\n", opcode); -#endif - switch(opcode) - { - case 0: { - // no op - CmiAbort("No op not implemented"); - } - case 1: { - // addition - aum_name_t name1 = extract(cmd); - aum_name_t name2 = extract(cmd); - aum_array_t& c1 = lookup(name1); - aum_array_t& c2 = lookup(name2); - std::visit( - [&](auto& x, auto& y) { - using T = std::decay_t; - using V = std::decay_t; - aum_array_t res; - if constexpr(std::is_same_v) - res = x + y; - else - CmiAbort("Operation not permitted"); - insert(res_name, res); - }, c1, c2); - break; - } - case 2: { - // subtraction - aum_name_t name1 = extract(cmd); - aum_name_t name2 = extract(cmd); - aum_array_t& c1 = lookup(name1); - aum_array_t& c2 = lookup(name2); - std::visit( - [&](auto& x, auto& y) { - using T = std::decay_t; - using V = std::decay_t; - aum_array_t res; - if constexpr(std::is_same_v) - res = x - y; - else - CmiAbort("Operation not permitted"); - insert(res_name, res); - }, c1, c2); - break; - } - case 3: { - // multiply - aum_name_t name1 = extract(cmd); - aum_name_t name2 = extract(cmd); - aum_array_t& c1 = lookup(name1); - aum_array_t& c2 = lookup(name2); - std::visit( - [&](auto& x, auto& y) { - using T = std::decay_t; - using V = std::decay_t; - aum_array_t res; - if constexpr(std::is_same_v) - res = x * y; - else - CmiAbort("Operation not permitted"); - insert(res_name, res); - }, c1, c2); - break; - } - case 4: { - // division - aum_name_t name1 = extract(cmd); - aum_name_t name2 = extract(cmd); - aum_array_t& c1 = lookup(name1); - aum_array_t& c2 = lookup(name2); - std::visit( - [&](auto& x, auto& y) { - using T = std::decay_t; - using V = std::decay_t; - aum_array_t res; - if constexpr(std::is_same_v && - std::is_same_v) - res = x / y; - else - CmiAbort("Operation not permitted"); - insert(res_name, res); - }, c1, c2); - break; - } - case 5: { - // mat/vec mul - aum_name_t name1 = extract(cmd); - aum_name_t name2 = extract(cmd); - aum_array_t& c1 = lookup(name1); - aum_array_t& c2 = lookup(name2); - std::visit( - [&](auto& x, auto& y) { - using T = std::decay_t; - using V = std::decay_t; - aum_array_t res; - if constexpr(std::is_same_v && - std::is_same_v) - // Matmul here - CmiAbort("Matrix multiplication not yet implemented"); - else if constexpr((std::is_same_v || - std::is_same_v) && - std::is_same_v) - res = aum::dot(x, y); - else - CmiAbort("Operation not permitted"); - insert(res_name, res); - }, c1, c2); - break; - } - // FIXME implement a matrix copy - case 6: { - aum_name_t name1 = extract(cmd); - aum_array_t& c1 = lookup(name1); - std::visit( - [&](auto& x) { - using T = std::decay_t; - if constexpr(std::is_same_v || - std::is_same_v) - { - aum_array_t res = aum::copy(x); - insert(res_name, res); - } - else - { - CmiAbort("Matrix copy not implemented!"); - } - }, c1); - break; - } - case 7: { - // axpy - aum_name_t name1 = extract(cmd); - aum_name_t name2 = extract(cmd); - aum_name_t name3 = extract(cmd); - aum_array_t& c1 = lookup(name1); - aum_array_t& c2 = lookup(name2); - aum_array_t& c3 = lookup(name3); - std::visit( - [&](auto& a, auto& x, auto& y) { - using S = std::decay_t; - using T = std::decay_t; - using V = std::decay_t; - aum_array_t res; - if constexpr(std::is_same_v && - std::is_same_v && - std::is_same_v) - res = aum::blas::axpy(a, x, y); - else - CmiAbort("Operation not permitted"); - insert(res_name, res); - }, c1, c2, c3); - break; - } - case 8: { - // axpy multiplier - aum_name_t name1 = extract(cmd); - aum_name_t name2 = extract(cmd); - aum_name_t name3 = extract(cmd); - double multiplier = extract(cmd); - aum_array_t& c1 = lookup(name1); - aum_array_t& c2 = lookup(name2); - aum_array_t& c3 = lookup(name3); - std::visit( - [&](auto& a, auto& x, auto& y) { - using S = std::decay_t; - using T = std::decay_t; - using V = std::decay_t; - aum_array_t res; - if constexpr(std::is_same_v && - std::is_same_v && - std::is_same_v) - res = aum::blas::axpy(multiplier, a, x, y); - else - CmiAbort("Operation not permitted"); - insert(res_name, res); - }, c1, c2, c3); - break; - } - case 9: { - // multiply - aum_name_t name1 = extract(cmd); - double y = extract(cmd); - aum_array_t& c1 = lookup(name1); - std::visit( - [&](auto& x) { - using T = std::decay_t; - aum_array_t res; - res = y * x; - insert(res_name, res); - }, c1); - break; - } - case 10: { - // division - aum_name_t name1 = extract(cmd); - aum_name_t name2 = extract(cmd); - aum_array_t& c1 = lookup(name1); - aum_array_t& c2 = lookup(name2); - std::visit( - [&](auto& x, auto& y) { - using T = std::decay_t; - using V = std::decay_t; - aum_array_t res; - if constexpr(std::is_same_v && - std::is_same_v) - res = x / y; - else - CmiAbort("Operation not permitted"); - insert(res_name, res); - }, c1, c2); - break; - } - default: { - CmiAbort("Operation not implemented"); - } - } - } - - static void creation_handler(char* msg) - { - /* First 32 bits are number of dimensions - * Each of the next 64 bits is the size of the array - * in each dimension - */ - // FIXME need a field for dtype - char* cmd = msg + CmiMsgHeaderSizeBytes; - uint8_t client_id = extract(cmd); - uint64_t epoch = extract(cmd); - uint32_t msg_size = CmiMsgHeaderSizeBytes + extract(cmd); - if (epoch > next_epoch[client_id]) - insert_buffer(msg_size, msg); - next_epoch[client_id]++; - aum_name_t res_name = extract(cmd); - uint32_t ndim = extract(cmd); - bool has_init = extract(cmd); - switch(ndim) - { - case 0: { - // create scalar - CmiAbort("Not implemented"); - } - case 1: { - // create vector - uint64_t size = extract(cmd); - aum_array_t res; - if (has_init) - { - double init_value = extract(cmd); - res = aum::vector(size, init_value); - } - else - { - res = aum::vector(size, aum::random{}); - } - insert(res_name, res); - break; - } - case 2: { - // create matrix - uint64_t size1 = extract(cmd); - uint64_t size2 = extract(cmd); - aum_array_t res; - if (has_init) - { - double init_value = extract(cmd); - res = aum::matrix(size1, size2, init_value); - } - else - { - res = aum::matrix(size1, size2, aum::random{}); - } - insert(res_name, res); - break; - } - default: { - CmiAbort("Greater than 2 dimensions not supported"); - } - } - } - - static void fetch_handler(char* msg) - { - char* cmd = msg + CmiMsgHeaderSizeBytes; - uint8_t client_id = extract(cmd); - uint64_t epoch = extract(cmd); - uint32_t msg_size = CmiMsgHeaderSizeBytes + extract(cmd); - if (epoch > next_epoch[client_id]) - insert_buffer(msg_size, msg); - next_epoch[client_id]++; - aum_name_t name = extract(cmd); - aum_array_t& arr = lookup(name); - void* reply = nullptr; - int reply_size = 0; - std::visit( - [&](auto& x) { - using T = std::decay_t; - if constexpr(std::is_same_v) - { - double value = x.get(); - reply = (void*) &value; - reply_size += 8; - CcsSendReply(reply_size, reply); - } - else - CmiAbort("Operation not implemented"); - }, arr); - } - - static void delete_handler(char* msg) - { - char* cmd = msg + CmiMsgHeaderSizeBytes; - uint8_t client_id = extract(cmd); - uint64_t epoch = extract(cmd); - uint32_t msg_size = CmiMsgHeaderSizeBytes + extract(cmd); - if (epoch > next_epoch[client_id]) - insert_buffer(msg_size, msg); - next_epoch[client_id]++; - aum_name_t name = extract(cmd); - remove(name); - } - - static void connection_handler(char* msg) - { - uint8_t client_id = get_client_id(); - CcsSendReply(1, (void*) &client_id); - } - - static void disconnection_handler(char* msg) - { - char* cmd = msg + CmiMsgHeaderSizeBytes; - uint8_t client_id = extract(cmd); - client_ids.push(client_id); -#ifndef NDEBUG - CkPrintf("Disconnected %" PRIu8 " from server\n", client_id); -#endif - } - - inline static void exit_server(char* msg) - { - CkExit(); - } - void register_handlers() { - CcsRegisterHandler("aum_connect", (CmiHandler) connection_handler); - CcsRegisterHandler("aum_disconnect", (CmiHandler) disconnection_handler); - CcsRegisterHandler("aum_operation", (CmiHandler) operation_handler); - CcsRegisterHandler("aum_creation", (CmiHandler) creation_handler); - CcsRegisterHandler("aum_fetch", (CmiHandler) fetch_handler); - CcsRegisterHandler("aum_delete", (CmiHandler) delete_handler); - CcsRegisterHandler("aum_exit", (CmiHandler) exit_server); + CcsRegisterHandler("aum_connect", (CmiHandler) Server::connection_handler); + CcsRegisterHandler("aum_disconnect", (CmiHandler) Server::disconnection_handler); + CcsRegisterHandler("aum_operation", (CmiHandler) Server::operation_handler); + CcsRegisterHandler("aum_creation", (CmiHandler) Server::creation_handler); + CcsRegisterHandler("aum_fetch", (CmiHandler) Server::fetch_handler); + CcsRegisterHandler("aum_delete", (CmiHandler) Server::delete_handler); + CcsRegisterHandler("aum_exit", (CmiHandler) Server::exit_server); } }; diff --git a/src/server.h b/src/server.h deleted file mode 100644 index 3e02f94..0000000 --- a/src/server.h +++ /dev/null @@ -1,28 +0,0 @@ -#include - -template -inline T extract(char* &msg, bool increment=true) -{ - T arg = *(reinterpret_cast(msg)); - if (increment) - msg += sizeof(T); - return arg; -} - -inline uint64_t extract_epoch(char* msg) -{ - char* cmd = msg + CmiMsgHeaderSizeBytes; - return extract(cmd); -} - -class epoch_compare -{ -public: - bool operator()(char* msg1, char* msg2) - { - uint64_t epoch1 = extract_epoch(msg1); - uint64_t epoch2 = extract_epoch(msg2); - return epoch1 < epoch2; - } -}; - diff --git a/src/server.hpp b/src/server.hpp new file mode 100644 index 0000000..686d5c8 --- /dev/null +++ b/src/server.hpp @@ -0,0 +1,299 @@ +#include +#include +#include +#include +#include +#include +#include "ast.hpp" + + +using aum_name_t = uint64_t; +using aum_array_t = std::variant; +std::unordered_map symbol_table; +std::stack client_ids; + + +aum_array_t calculate(astnode* node, std::vector &metadata); + + +class Server +{ +public: + static void initialize() + { + for (int16_t i = 255; i >= 0; i--) + client_ids.push((uint8_t) i); + } + + inline static void insert(aum_name_t name, aum_array_t arr) + { +#ifndef NDEBUG + CkPrintf("Created array %" PRIu64 " on server\n", name); +#endif + symbol_table.emplace(name, arr); + } + + inline static void remove(aum_name_t name) + { + symbol_table.erase(name); +#ifndef NDEBUG + CkPrintf("Deleted array %" PRIu64 " on server\n", name); +#endif + } + + inline static uint8_t get_client_id() + { + if (client_ids.empty()) + CmiAbort("Too many clients connected to the server"); + uint8_t client_id = client_ids.top(); + client_ids.pop(); + return client_id; + } + + static aum_array_t& lookup(aum_name_t name) + { + auto find = symbol_table.find(name); + if (find == std::end(symbol_table)) + { +#ifndef NDEBUG + CkPrintf("Active symbols: "); + for (auto it: symbol_table) + CkPrintf("%" PRIu64 ", ", it.first); + CkPrintf("\n"); +#endif + CmiAbort("Symbol %i not found", name); + } + return find->second; + } + + static void operation_handler(char* msg) + { + char* cmd = msg + CmiMsgHeaderSizeBytes; + astnode* head = decode(cmd); + std::vector metadata; + calculate(head, metadata); + delete_ast(head); + CcsSendReply(sizeof(uint64_t) * metadata.size(), (void*) &metadata[0]); + } + + static void creation_handler(char* msg) + { + /* First 32 bits are number of dimensions + * Each of the next 64 bits is the size of the array + * in each dimension + */ + // FIXME need a field for dtype + try + { + char* cmd = msg + CmiMsgHeaderSizeBytes; + aum_name_t res_name = extract(cmd); + uint32_t ndim = extract(cmd); + bool has_buf = extract(cmd); + bool has_init = extract(cmd); + switch(ndim) + { + case 0: { + // create scalar + CmiAbort("Not implemented"); + } + case 1: { + // create vector + uint64_t size = extract(cmd); + aum_array_t res; + if (has_buf) + { + double* init_buf = (double*) cmd; + res = aum::vector(size, init_buf); + } + else if (has_init) + { + double init_value = extract(cmd); + res = aum::vector(size, init_value); + } + else + { + res = aum::vector(size, aum::random{}); + } + insert(res_name, res); + break; + } + case 2: { + // create matrix + uint64_t size1 = extract(cmd); + uint64_t size2 = extract(cmd); + aum_array_t res; + if (has_buf) + { + double* init_buf = (double*) cmd; + res = aum::matrix(size1, size2, init_buf); + } + else if (has_init) + { + double init_value = extract(cmd); + res = aum::matrix(size1, size2, init_value); + } + else + { + res = aum::matrix(size1, size2, aum::random{}); + } + insert(res_name, res); + break; + } + default: { + // FIXME is this correctly caught? + CmiAbort("Greater than 2 dimensions not supported"); + } + } + + bool status = true; + CcsSendReply(1, (void*) &status); + } + catch(...) + { + bool status = false; + CcsSendReply(1, (void*) &status); + } + } + + static void fetch_handler(char* msg) + { + char* cmd = msg + CmiMsgHeaderSizeBytes; + aum_name_t name = extract(cmd); + aum_array_t& arr = lookup(name); + void* reply = nullptr; + int reply_size = 0; + std::visit( + [&](auto& x) { + using T = std::decay_t; + if constexpr(std::is_same_v) + { + double value = x.get(); + reply = (void*) &value; + reply_size += 8; + CcsSendReply(reply_size, reply); + } + else + CmiAbort("Operation not implemented"); + }, arr); + } + + static void delete_handler(char* msg) + { + char* cmd = msg + CmiMsgHeaderSizeBytes; + aum_name_t name = extract(cmd); + remove(name); + } + + static void connection_handler(char* msg) + { + uint8_t client_id = get_client_id(); + CcsSendReply(1, (void*) &client_id); + } + + static void disconnection_handler(char* msg) + { + char* cmd = msg + CmiMsgHeaderSizeBytes; + uint8_t client_id = extract(cmd); + client_ids.push(client_id); +#ifndef NDEBUG + CkPrintf("Disconnected %" PRIu8 " from server\n", client_id); +#endif + } + + inline static void exit_server(char* msg) + { + CkExit(); + } +}; + +void extract_metadata(aum_name_t name, aum_array_t &res, std::vector &metadata) +{ + metadata.push_back(name); + std::visit( + [&](auto& x) { + using T = std::decay_t; + if constexpr (std::is_same_v) + { + metadata.push_back(x.rows()); + metadata.push_back(x.cols()); + } + else if constexpr (std::is_same_v) + metadata.push_back(x.size()); + else if constexpr (std::is_same_v) + metadata.push_back(1); + else + CmiAbort("Array type not recognized"); + }, res); +} + +aum_array_t calculate(astnode* node, std::vector &metadata) +{ + switch (node->oper) + { + case operation::noop: { + return Server::lookup(node->name); + } + case operation::add: { + aum_array_t s1 = calculate(node->operands[0], metadata); + aum_array_t s2 = calculate(node->operands[1], metadata); + aum_array_t res; + + std::visit( + [&](auto& x, auto& y) { + using T = std::decay_t; + using V = std::decay_t; + if constexpr(std::is_same_v) + { + if (!node->operands[0]->store) + res = x.add_inplace(y); + else if (!node->operands[1]->store) + res = y.add_inplace(x); + else + res = x + y; + } + else + CmiAbort("Operation not permitted"); + }, s1, s2); + + if (node->store) + { + Server::insert(node->name, res); + extract_metadata(node->name, res, metadata); + } + return res; + } + case operation::sub: { + aum_array_t s1 = calculate(node->operands[0], metadata); + aum_array_t s2 = calculate(node->operands[1], metadata); + aum_array_t res; + + std::visit( + [&](auto& x, auto& y) { + using T = std::decay_t; + using V = std::decay_t; + if constexpr(std::is_same_v) + { + if (!node->operands[0]->store) + res = x.sub_inplace_1(y); + else if (!node->operands[1]->store) + res = y.sub_inplace_2(x); + else + res = x - y; + } + else + CmiAbort("Operation not permitted"); + }, s1, s2); + + if (node->store) + { + Server::insert(node->name, res); + extract_metadata(node->name, res, metadata); + } + return res; + } + default: { + CmiAbort("Operation not implemented"); + } + } +} + From d2499811f470d072798f9d944781875bf0fa17fd Mon Sep 17 00:00:00 2001 From: Aditya Bhosale Date: Thu, 2 Jun 2022 23:12:18 -0500 Subject: [PATCH 12/16] Add more operations with lazy evaluation --- examples/graph.py | 17 +++++++---- pyproject/array.py | 17 +++-------- pyproject/ast.py | 30 ++++++++++++++----- pyproject/ccs.py | 17 +++++++---- src/ast.hpp | 20 ++++++++++--- src/server.hpp | 75 +++++++++++++++++++++++++++++++++++++++++++--- 6 files changed, 135 insertions(+), 41 deletions(-) diff --git a/examples/graph.py b/examples/graph.py index dc6d91a..4458880 100644 --- a/examples/graph.py +++ b/examples/graph.py @@ -6,14 +6,19 @@ def f(): v = ndarray(1, 10, np.float64) b = ndarray(1, 10, np.float64) c = ndarray(1, 10, np.float64) - y = v + b + c - z = v - y - z.evaluate() - print(z.shape) + w = c + for i in range(5): + y = v + b + w + z = v - y + w = 2 * (c - z) + b + w.command_buffer.plot_graph() + #w.evaluate() + #print(w.shape) + #print(z.shape) + #print(y.shape) if __name__ == '__main__': - connect("172.17.0.1", 10000) + #connect("172.17.0.1", 10000) s = f() - diff --git a/pyproject/array.py b/pyproject/array.py index e924964..d3a04fe 100644 --- a/pyproject/array.py +++ b/pyproject/array.py @@ -1,4 +1,5 @@ import sys +import warnings import numpy as np from pyproject.ast import ASTNode from pyproject.ccs import to_bytes, from_bytes, send_command_raw, send_command, \ @@ -49,7 +50,7 @@ def __init__(self, ndim, shape=None, dtype=np.float64, init_value=None, cmd = get_creation_command(self, self.name, buf=buf) if not send_command(Handlers.creation_handler, cmd, reply_type='?'): - raise RuntimeError("Error creating array on server") + warnings.warn("Error creating array on server", RuntimeWarning) self.command_buffer = ASTNode(self.name, 0, [self]) else: self.name = name @@ -60,8 +61,6 @@ def shape(self): return self._shape def __del__(self): - from pyproject.ccs import client_id - global client_id if self.valid: cmd = to_bytes(self.name, 'L') send_command_async(Handlers.delete_handler, cmd) @@ -101,12 +100,8 @@ def __rsub__(self, other): def __mul__(self, other): if self.ndim > 0 or (isinstance(other, ndarray) and other.ndim > 0): RuntimeError("Cannote multiply two arrays") - if isinstance(other, ndarray): - op = '*' - elif isinstance(other, float) or isinstance(other, int): - op = '*s' res = get_name() - cmd_buffer = ASTNode(res, OPCODES.get(op), [self, other]) + cmd_buffer = ASTNode(res, OPCODES.get('*'), [self, other]) return create_ndarray(self.ndim, self.dtype, name=res, command_buffer=cmd_buffer) @@ -116,12 +111,8 @@ def __rmul__(self, other): def __truediv__(self, other): if self.ndim > 0 or other.ndim > 0: RuntimeError("Cannote divide two arrays") - if isinstance(other, ndarray): - op = '/' - elif isinstance(other, float) or isinstance(other, int): - op = '/s' res = get_name() - cmd_buffer = ASTNode(res, OPCODES.get(op), [self, other]) + cmd_buffer = ASTNode(res, OPCODES.get('/'), [self, other]) return create_ndarray(self.ndim, self.dtype, name=res, command_buffer=cmd_buffer) diff --git a/pyproject/ast.py b/pyproject/ast.py index 2a0deae..f73a2ec 100644 --- a/pyproject/ast.py +++ b/pyproject/ast.py @@ -18,6 +18,7 @@ def get_command(self, validated_arrays, save=True): from pyproject.array import ndarray if self.opcode == 0: cmd = to_bytes(self.opcode, 'L') + cmd += to_bytes(False, '?') cmd += to_bytes(self.operands[0].name, 'L') return cmd cmd = to_bytes(self.opcode, 'L') + to_bytes(self.name, 'L') @@ -28,16 +29,19 @@ def get_command(self, validated_arrays, save=True): save_op = True if c_long.from_address(id(op)).value - 2 > 0 else False opcmd = op.command_buffer.get_command(validated_arrays, save=save_op) - if save_op: + if not op.valid and save_op: validated_arrays[op.name] = op cmd += to_bytes(len(opcmd), 'I') cmd += opcmd elif isinstance(op, float) or isinstance(op, int): - cmd += to_bytes(8, 'I') - cmd += to_bytes(op, 'd') + opcmd = to_bytes(0, 'L') + opcmd += to_bytes(True, '?') + opcmd += to_bytes(op, 'd') + cmd += to_bytes(len(opcmd), 'I') + cmd += opcmd return cmd - def plot_graph(self, G=None, node_map={}, next_id=0, parent=None): + def plot_graph(self, G=None, node_map={}, color_map={}, next_id=0, parent=None): from pyproject.array import ndarray if G is None: G = nx.Graph() @@ -49,19 +53,24 @@ def plot_graph(self, G=None, node_map={}, next_id=0, parent=None): next_id += 1 if not self.operands[0].valid: next_id = self.operands[0].command_buffer.plot_graph( - G, node_map, next_id, next_id - 1) + G, node_map, color_map, next_id, next_id - 1) return next_id opnode = next_id G.add_node(next_id) if parent is not None: G.add_edge(parent, next_id) node_map[next_id] = INV_OPCODES.get(self.opcode, '?') + if parent is None: + color_map[next_id] = 1 next_id += 1 for op in self.operands: # an operand can also be a double if isinstance(op, ndarray): + save_op = True if c_long.from_address(id(op)).value - 2 > 0 else False + if not op.valid and save_op: + color_map[next_id] = 1 next_id = op.command_buffer.plot_graph( - G, node_map, next_id, opnode) + G, node_map, color_map, next_id, opnode) elif isinstance(op, float) or isinstance(op, int): G.add_node(next_id) G.add_edge(opnode, next_id) @@ -69,8 +78,13 @@ def plot_graph(self, G=None, node_map={}, next_id=0, parent=None): next_id += 1 if parent is None: pos = graphviz_layout(G, prog="dot") - nx.draw(G, pos, labels=node_map) + color_map_list = [] + for node in G: + if node in color_map: + color_map_list.append('tab:red') + else: + color_map_list.append('tab:blue') + nx.draw(G, pos, labels=node_map, node_color=color_map_list) plt.show() return next_id - diff --git a/pyproject/ccs.py b/pyproject/ccs.py index 67ef8e1..d5914d4 100644 --- a/pyproject/ccs.py +++ b/pyproject/ccs.py @@ -4,11 +4,11 @@ from pyproject import array server = None -client_id = None +client_id = 0 next_name = 0 OPCODES = {'+': 1, '-': 2, '*': 3 ,'/': 4, '@': 5, 'copy': 6, 'axpy': 7, - 'axpy_multiplier': 8, '*s': 9, '/s': 10} + 'axpy_multiplier': 8} INV_OPCODES = {v: k for k, v in OPCODES.items()} @@ -25,14 +25,19 @@ def from_bytes(bvalue, dtype='I'): return struct.unpack(dtype, bvalue)[0] def send_command_raw(handler, msg, reply_size): - server.send_request(handler, 0, msg) - return server.receive_response(reply_size) + if server is not None: + server.send_request(handler, 0, msg) + return server.receive_response(reply_size) def send_command(handler, msg, reply_size=1, reply_type='B'): - return from_bytes(send_command_raw(handler, msg, reply_size), reply_type) + global server + if server is not None: + return from_bytes(send_command_raw(handler, msg, reply_size), reply_type) def send_command_async(handler, msg): - server.send_request(handler, 0, msg) + global server + if server is not None: + server.send_request(handler, 0, msg) def connect(server_ip, server_port): global server, client_id diff --git a/src/ast.hpp b/src/ast.hpp index 5e77eab..22da582 100644 --- a/src/ast.hpp +++ b/src/ast.hpp @@ -1,5 +1,6 @@ #include #include +#include template @@ -22,9 +23,7 @@ enum class operation : uint64_t matmul = 5, copy = 6, axpy = 7, - axpy_multiplier = 8, - mul_scalar = 9, - div_scalar = 10 + axpy_multiplier = 8 }; @@ -32,6 +31,7 @@ class astnode { public: bool store; + bool is_scalar; uint64_t name; operation oper; std::vector operands; @@ -49,10 +49,22 @@ astnode* decode(char* cmd) uint64_t opcode = extract(cmd); astnode* node = new astnode; node->oper = lookup_operation(opcode); - node->name = extract(cmd); if (opcode == 0) + { + node->is_scalar = extract(cmd); + // if leaf is a scalar + if (node->is_scalar) + { + double value = extract(cmd); + memcpy(&(node->name), &value, sizeof(double)); + } + else + node->name = extract(cmd); return node; + } + node->is_scalar = false; + node->name = extract(cmd); node->store = extract(cmd); uint8_t num_operands = extract(cmd); for (uint8_t i = 0; i < num_operands; i++) diff --git a/src/server.hpp b/src/server.hpp index 686d5c8..f55969d 100644 --- a/src/server.hpp +++ b/src/server.hpp @@ -8,7 +8,7 @@ using aum_name_t = uint64_t; -using aum_array_t = std::variant; +using aum_array_t = std::variant; std::unordered_map symbol_table; std::stack client_ids; @@ -231,7 +231,10 @@ aum_array_t calculate(astnode* node, std::vector &metadata) switch (node->oper) { case operation::noop: { - return Server::lookup(node->name); + if (node->is_scalar) + return static_cast(node->name); + else + return Server::lookup(node->name); } case operation::add: { aum_array_t s1 = calculate(node->operands[0], metadata); @@ -242,7 +245,7 @@ aum_array_t calculate(astnode* node, std::vector &metadata) [&](auto& x, auto& y) { using T = std::decay_t; using V = std::decay_t; - if constexpr(std::is_same_v) + if constexpr(std::is_same_v && !std::is_same_v) { if (!node->operands[0]->store) res = x.add_inplace(y); @@ -271,7 +274,7 @@ aum_array_t calculate(astnode* node, std::vector &metadata) [&](auto& x, auto& y) { using T = std::decay_t; using V = std::decay_t; - if constexpr(std::is_same_v) + if constexpr(std::is_same_v && !std::is_same_v) { if (!node->operands[0]->store) res = x.sub_inplace_1(y); @@ -291,6 +294,70 @@ aum_array_t calculate(astnode* node, std::vector &metadata) } return res; } + case operation::mul: { + aum_array_t s1 = calculate(node->operands[0], metadata); + aum_array_t s2 = calculate(node->operands[1], metadata); + aum_array_t res; + + std::visit( + [&](auto& x, auto& y) { + using T = std::decay_t; + using V = std::decay_t; + if constexpr(std::is_same_v || + std::is_same_v) + { + // FIXME implement inplace multiply + // if (!node->operands[1]->store) + // res = y.mul_inplace(x); + // else + res = x * y; + } + else if constexpr(std::is_same_v || + std::is_same_v) + { + // if (!node->operands[0]->store) + // res = x.mul_inplace(y); + // else + res = y * x; + } + else + CmiAbort("Operation not permitted"); + }, s1, s2); + + if (node->store) + { + Server::insert(node->name, res); + extract_metadata(node->name, res, metadata); + } + return res; + } + case operation::div: { + aum_array_t s1 = calculate(node->operands[0], metadata); + aum_array_t s2 = calculate(node->operands[1], metadata); + aum_array_t res; + + std::visit( + [&](auto& x, auto& y) { + using T = std::decay_t; + using V = std::decay_t; + if constexpr((std::is_same_v || + std::is_same_v) && + (std::is_same_v || + std::is_same_v)) + { + res = x / y; + } + else + CmiAbort("Operation not permitted"); + }, s1, s2); + + if (node->store) + { + Server::insert(node->name, res); + extract_metadata(node->name, res, metadata); + } + return res; + } default: { CmiAbort("Operation not implemented"); } From 87f70a3af975f9bc6d0ed3ae3b1e70a2df351bac Mon Sep 17 00:00:00 2001 From: Aditya Bhosale Date: Fri, 3 Jun 2022 18:58:42 -0500 Subject: [PATCH 13/16] Get conjugate gradient working --- examples/graph.py | 3 +- pyproject/ast.py | 59 ++++++++++++++--------- pyproject/linalg.py | 5 +- src/ast.hpp | 2 + src/server.hpp | 112 ++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 150 insertions(+), 31 deletions(-) diff --git a/examples/graph.py b/examples/graph.py index 4458880..42fc3f4 100644 --- a/examples/graph.py +++ b/examples/graph.py @@ -4,7 +4,7 @@ def f(): v = ndarray(1, 10, np.float64) - b = ndarray(1, 10, np.float64) + b = ndarray(1, 10, np.float64, init_value=10) c = ndarray(1, 10, np.float64) w = c for i in range(5): @@ -12,7 +12,6 @@ def f(): z = v - y w = 2 * (c - z) + b w.command_buffer.plot_graph() - #w.evaluate() #print(w.shape) #print(z.shape) #print(y.shape) diff --git a/pyproject/ast.py b/pyproject/ast.py index f73a2ec..0c729fd 100644 --- a/pyproject/ast.py +++ b/pyproject/ast.py @@ -26,13 +26,20 @@ def get_command(self, validated_arrays, save=True): for op in self.operands: # an operand can also be a double if isinstance(op, ndarray): - save_op = True if c_long.from_address(id(op)).value - 2 > 0 else False - opcmd = op.command_buffer.get_command(validated_arrays, - save=save_op) - if not op.valid and save_op: - validated_arrays[op.name] = op - cmd += to_bytes(len(opcmd), 'I') - cmd += opcmd + if op.name in validated_arrays: + opcmd = to_bytes(0, 'L') + opcmd += to_bytes(False, '?') + opcmd += to_bytes(op.name, 'L') + cmd += to_bytes(len(opcmd), 'I') + cmd += opcmd + else: + save_op = True if c_long.from_address(id(op)).value - 2 > 0 else False + opcmd = op.command_buffer.get_command(validated_arrays, + save=save_op) + if not op.valid and save_op: + validated_arrays[op.name] = op + cmd += to_bytes(len(opcmd), 'I') + cmd += opcmd elif isinstance(op, float) or isinstance(op, int): opcmd = to_bytes(0, 'L') opcmd += to_bytes(True, '?') @@ -41,7 +48,8 @@ def get_command(self, validated_arrays, save=True): cmd += opcmd return cmd - def plot_graph(self, G=None, node_map={}, color_map={}, next_id=0, parent=None): + def plot_graph(self, validated_arrays={}, G=None, node_map={}, + color_map={}, next_id=0, parent=None): from pyproject.array import ndarray if G is None: G = nx.Graph() @@ -53,24 +61,33 @@ def plot_graph(self, G=None, node_map={}, color_map={}, next_id=0, parent=None): next_id += 1 if not self.operands[0].valid: next_id = self.operands[0].command_buffer.plot_graph( - G, node_map, color_map, next_id, next_id - 1) + validated_arrays, G, node_map, color_map, next_id, + next_id - 1) return next_id opnode = next_id G.add_node(next_id) if parent is not None: G.add_edge(parent, next_id) - node_map[next_id] = INV_OPCODES.get(self.opcode, '?') + node_map[next_id] = INV_OPCODES.get(self.opcode, '?') + (': a%i' % self.name) if parent is None: - color_map[next_id] = 1 + color_map[next_id] = 'tab:red' next_id += 1 for op in self.operands: # an operand can also be a double if isinstance(op, ndarray): - save_op = True if c_long.from_address(id(op)).value - 2 > 0 else False - if not op.valid and save_op: - color_map[next_id] = 1 - next_id = op.command_buffer.plot_graph( - G, node_map, color_map, next_id, opnode) + if op.name in validated_arrays: + G.add_node(next_id) + G.add_edge(opnode, next_id) + node_map[next_id] = 'a' + str(op.name) + color_map[next_id] = 'tab:green' + next_id += 1 + else: + save_op = True if c_long.from_address(id(op)).value - 2 > 0 else False + if not op.valid and save_op: + color_map[next_id] = 'tab:red' + validated_arrays[op.name] = op + next_id = op.command_buffer.plot_graph( + validated_arrays, G, node_map, color_map, next_id, opnode) elif isinstance(op, float) or isinstance(op, int): G.add_node(next_id) G.add_edge(opnode, next_id) @@ -78,13 +95,9 @@ def plot_graph(self, G=None, node_map={}, color_map={}, next_id=0, parent=None): next_id += 1 if parent is None: pos = graphviz_layout(G, prog="dot") - color_map_list = [] - for node in G: - if node in color_map: - color_map_list.append('tab:red') - else: - color_map_list.append('tab:blue') - nx.draw(G, pos, labels=node_map, node_color=color_map_list) + color_map_list = [color_map.get(node, 'tab:blue') for node in G] + nx.draw(G, pos, labels=node_map, node_color=color_map_list, + node_size=600, font_size=10) plt.show() return next_id diff --git a/pyproject/linalg.py b/pyproject/linalg.py index 456a151..c5d1e9b 100644 --- a/pyproject/linalg.py +++ b/pyproject/linalg.py @@ -2,8 +2,9 @@ import struct import numpy as np from pyccs import Server -from pyproject.ccs import get_name, send_command, Handlers +from pyproject.ccs import OPCODES, get_name, send_command, Handlers from pyproject.array import create_ndarray +from pyproject.ast import ASTNode def axpy(a, x, y, multiplier=None): @@ -15,7 +16,7 @@ def axpy(a, x, y, multiplier=None): operation = 'axpy' res = get_name() cmd_buffer = ASTNode(res, OPCODES.get(operation), operands) - return create_ndarray(self.ndim, self.dtype, + return create_ndarray(x.ndim, x.dtype, name=res, command_buffer=cmd_buffer) diff --git a/src/ast.hpp b/src/ast.hpp index 22da582..1937ec6 100644 --- a/src/ast.hpp +++ b/src/ast.hpp @@ -32,6 +32,8 @@ class astnode public: bool store; bool is_scalar; + // FIXME double scalars fit into name, but should probably + // handle this better uint64_t name; operation oper; std::vector operands; diff --git a/src/server.hpp b/src/server.hpp index f55969d..27d773a 100644 --- a/src/server.hpp +++ b/src/server.hpp @@ -219,9 +219,7 @@ void extract_metadata(aum_name_t name, aum_array_t &res, std::vector & } else if constexpr (std::is_same_v) metadata.push_back(x.size()); - else if constexpr (std::is_same_v) - metadata.push_back(1); - else + else if constexpr (!std::is_same_v) CmiAbort("Array type not recognized"); }, res); } @@ -232,7 +230,7 @@ aum_array_t calculate(astnode* node, std::vector &metadata) { case operation::noop: { if (node->is_scalar) - return static_cast(node->name); + return *reinterpret_cast(&(node->name)); else return Server::lookup(node->name); } @@ -358,6 +356,112 @@ aum_array_t calculate(astnode* node, std::vector &metadata) } return res; } + case operation::matmul: { + aum_array_t s1 = calculate(node->operands[0], metadata); + aum_array_t s2 = calculate(node->operands[1], metadata); + aum_array_t res; + + std::visit( + [&](auto& x, auto& y) { + using T = std::decay_t; + using V = std::decay_t; + if constexpr(std::is_same_v && + std::is_same_v) + CmiAbort("Matrix multiplication not yet implemented"); + else if constexpr((std::is_same_v || + std::is_same_v) && + std::is_same_v) + { + // TODO implement inplace dot + res = aum::dot(x, y); + } + else + CmiAbort("Operation not permitted"); + }, s1, s2); + + if (node->store) + { + Server::insert(node->name, res); + extract_metadata(node->name, res, metadata); + } + return res; + } + case operation::copy: { + aum_array_t s1 = calculate(node->operands[0], metadata); + aum_array_t res; + + std::visit( + [&](auto& x) { + using T = std::decay_t; + if constexpr(std::is_same_v || + std::is_same_v) + res = aum::copy(x); + else + CmiAbort("Matrix copy not implemented"); + }, s1); + + if (node->store) + { + Server::insert(node->name, res); + extract_metadata(node->name, res, metadata); + } + return res; + } + case operation::axpy: { + aum_array_t s1 = calculate(node->operands[0], metadata); + aum_array_t s2 = calculate(node->operands[1], metadata); + aum_array_t s3 = calculate(node->operands[2], metadata); + aum_array_t res; + + std::visit( + [&](auto& a, auto& x, auto& y) { + using S = std::decay_t; + using T = std::decay_t; + using V = std::decay_t; + if constexpr(std::is_same_v && + std::is_same_v && + std::is_same_v) + res = aum::blas::axpy(a, x, y); + else + CmiAbort("Operation not permitted"); + }, s1, s2, s3); + + if (node->store) + { + Server::insert(node->name, res); + extract_metadata(node->name, res, metadata); + } + return res; + } + case operation::axpy_multiplier: { + aum_array_t s1 = calculate(node->operands[0], metadata); + aum_array_t s2 = calculate(node->operands[1], metadata); + aum_array_t s3 = calculate(node->operands[2], metadata); + aum_array_t multiplier = calculate(node->operands[3], metadata); + aum_array_t res; + + std::visit( + [&](auto& multiplier, auto& a, auto& x, auto& y) { + using S = std::decay_t; + using T = std::decay_t; + using V = std::decay_t; + using M = std::decay_t; + if constexpr(std::is_same_v && + std::is_same_v && + std::is_same_v && + std::is_same_v) + res = aum::blas::axpy(multiplier, a, x, y); + else + CmiAbort("Operation not permitted"); + }, multiplier, s1, s2, s3); + + if (node->store) + { + Server::insert(node->name, res); + extract_metadata(node->name, res, metadata); + } + return res; + } default: { CmiAbort("Operation not implemented"); } From a00f451a86dccaad93099e3f1a29567010137920 Mon Sep 17 00:00:00 2001 From: Aditya Bhosale Date: Tue, 7 Jun 2022 23:25:10 -0500 Subject: [PATCH 14/16] Add debug mode and inplace dot --- examples/conjugate_gradient.py | 16 ++++++++--- examples/graph.py | 12 +++++---- pyproject/array.py | 49 +++++++++++++++++++++++----------- pyproject/ast.py | 41 +++++++++++++++++++--------- pyproject/ccs.py | 28 ++++++++++++++----- src/server.hpp | 14 ++++++++-- 6 files changed, 114 insertions(+), 46 deletions(-) diff --git a/examples/conjugate_gradient.py b/examples/conjugate_gradient.py index 094ecbe..e0c7c9a 100644 --- a/examples/conjugate_gradient.py +++ b/examples/conjugate_gradient.py @@ -1,14 +1,19 @@ from pyproject.array import connect, ndarray import pyproject.linalg as lg +from pyproject.ccs import enable_debug import numpy as np +import time + +enable_debug() + def solve(A, b): - x = ndarray(1, 100, np.float64) + x = ndarray(1, 1000, np.float64) r = b - A @ x p = r.copy() rsold = r @ r - for i in range(100): + for i in range(1000): Ap = A @ p alpha = rsold / (p @ Ap) @@ -29,8 +34,11 @@ def solve(A, b): if __name__ == '__main__': connect("172.17.0.1", 10000) - A = ndarray(2, (100, 100), np.float64) - b = ndarray(1, 100, np.float64) + A = ndarray(2, (1000, 1000), np.float64, init_value=1.) + b = ndarray(1, 1000, np.float64, init_value=1.) + + start = time.time() x = solve(A, b) + print("Execution time = %.6f" % (time.time() - start)) diff --git a/examples/graph.py b/examples/graph.py index 42fc3f4..6054c62 100644 --- a/examples/graph.py +++ b/examples/graph.py @@ -1,7 +1,12 @@ from pyproject.array import connect, ndarray +from pyproject.ast import set_max_depth +from pyproject.ccs import enable_debug import pyproject.linalg as lg import numpy as np +#enable_debug() +set_max_depth(100) + def f(): v = ndarray(1, 10, np.float64) b = ndarray(1, 10, np.float64, init_value=10) @@ -11,13 +16,10 @@ def f(): y = v + b + w z = v - y w = 2 * (c - z) + b - w.command_buffer.plot_graph() - #print(w.shape) - #print(z.shape) - #print(y.shape) + w.evaluate() if __name__ == '__main__': - #connect("172.17.0.1", 10000) + connect("172.17.0.1", 10000) s = f() diff --git a/pyproject/array.py b/pyproject/array.py index d3a04fe..f7037d8 100644 --- a/pyproject/array.py +++ b/pyproject/array.py @@ -1,10 +1,10 @@ import sys import warnings import numpy as np -from pyproject.ast import ASTNode +from pyproject.ast import get_max_depth, ASTNode from pyproject.ccs import to_bytes, from_bytes, send_command_raw, send_command, \ send_command_async, connect, get_creation_command, \ - get_name, get_fetch_command, Handlers, OPCODES + get_name, get_fetch_command, Handlers, OPCODES, is_debug def create_ndarray(ndim, dtype, shape=None, name=None, command_buffer=None): @@ -12,6 +12,11 @@ def create_ndarray(ndim, dtype, shape=None, name=None, command_buffer=None): command_buffer=command_buffer) +def from_numpy(nparr): + return ndarray(nparr.ndim, dtype=nparr.dtype, shape=nparr.shape, + nparr=nparr) + + class ndarray: def __init__(self, ndim, shape=None, dtype=np.float64, init_value=None, nparr=None, name=None, command_buffer=None): @@ -47,13 +52,19 @@ def __init__(self, ndim, shape=None, dtype=np.float64, init_value=None, buf = nparr.tobytes() else: buf = None - cmd = get_creation_command(self, self.name, buf=buf) + cmd = get_creation_command(self, self.name, self._shape, buf=buf) if not send_command(Handlers.creation_handler, cmd, reply_type='?'): warnings.warn("Error creating array on server", RuntimeWarning) self.command_buffer = ASTNode(self.name, 0, [self]) else: self.name = name + max_depth = get_max_depth() + if self.command_buffer.depth > max_depth: + if is_debug(): + print("Maximum AST depth exceeded for %i, " + "flushing buffer" % self.name) + self._flush_command_buffer() @property def shape(self): @@ -69,7 +80,7 @@ def __len__(self): return self.shape[0] def __str__(self): - pass + print(self.get()) #def __repr__(self): # #self._flush_command_buffer() @@ -133,6 +144,9 @@ def __matmul__(self, other): def _flush_command_buffer(self): # send the command to server # finally set command buffer to array name + debug = is_debug() + if debug: + self.command_buffer.plot_graph() if self.valid: return validated_arrays = {self.name : self} @@ -140,18 +154,22 @@ def _flush_command_buffer(self): reply_size = 0 for name, arr in validated_arrays.items(): reply_size += 8 + 8 * arr.ndim - metadata = send_command_raw(Handlers.operation_handler, - cmd, reply_size=reply_size) - # traverse metadata - offset = 0 - for i in range(len(validated_arrays)): - name = from_bytes(metadata[offset : offset + 8], 'L') - offset += 8 - arr = validated_arrays[name] - for d in range(arr.ndim): - arr._shape[d] = from_bytes(metadata[offset : offset + 8], 'L') + if not debug: + metadata = send_command_raw(Handlers.operation_handler, + cmd, reply_size=reply_size) + # traverse metadata + offset = 0 + for i in range(len(validated_arrays)): + name = from_bytes(metadata[offset : offset + 8], 'L') offset += 8 - arr.validate() + arr = validated_arrays[name] + for d in range(arr.ndim): + arr._shape[d] = from_bytes(metadata[offset : offset + 8], 'L') + offset += 8 + arr.validate() + else: + for name, arr in validated_arrays.items(): + arr.validate() self.validate() def get(self): @@ -180,4 +198,3 @@ def copy(self): return create_ndarray(self.ndim, self.dtype, name=res, command_buffer=cmd_buffer) - diff --git a/pyproject/ast.py b/pyproject/ast.py index 0c729fd..4d73195 100644 --- a/pyproject/ast.py +++ b/pyproject/ast.py @@ -6,13 +6,32 @@ from pyproject.ccs import OPCODES, INV_OPCODES, to_bytes +max_depth = 10 + + +def set_max_depth(d): + global max_depth + max_depth = d + + +def get_max_depth(): + global max_depth + return max_depth + + class ASTNode(object): def __init__(self, name, opcode, operands): + from pyproject.array import ndarray # contains opcode, operands # operands are ndarrays self.name = name self.opcode = opcode self.operands = operands + self.depth = 0 + if self.opcode != 0: + for op in self.operands: + if isinstance(op, ndarray): + self.depth = max(self.depth, 1 + op.command_buffer.depth) def get_command(self, validated_arrays, save=True): from pyproject.array import ndarray @@ -44,12 +63,13 @@ def get_command(self, validated_arrays, save=True): opcmd = to_bytes(0, 'L') opcmd += to_bytes(True, '?') opcmd += to_bytes(op, 'd') + #print(opcmd, len(opcmd), len(cmd)) cmd += to_bytes(len(opcmd), 'I') cmd += opcmd return cmd def plot_graph(self, validated_arrays={}, G=None, node_map={}, - color_map={}, next_id=0, parent=None): + color_map={}, next_id=0, parent=None, save=True): from pyproject.array import ndarray if G is None: G = nx.Graph() @@ -58,19 +78,15 @@ def plot_graph(self, validated_arrays={}, G=None, node_map={}, G.add_node(next_id) if parent is not None: G.add_edge(parent, next_id) - next_id += 1 - if not self.operands[0].valid: - next_id = self.operands[0].command_buffer.plot_graph( - validated_arrays, G, node_map, color_map, next_id, - next_id - 1) - return next_id + return next_id + 1 opnode = next_id G.add_node(next_id) if parent is not None: G.add_edge(parent, next_id) - node_map[next_id] = INV_OPCODES.get(self.opcode, '?') + (': a%i' % self.name) - if parent is None: + node_map[next_id] = INV_OPCODES.get(self.opcode, '?') + if save: color_map[next_id] = 'tab:red' + node_map[next_id] += (': a%i' % self.name) next_id += 1 for op in self.operands: # an operand can also be a double @@ -84,17 +100,18 @@ def plot_graph(self, validated_arrays={}, G=None, node_map={}, else: save_op = True if c_long.from_address(id(op)).value - 2 > 0 else False if not op.valid and save_op: - color_map[next_id] = 'tab:red' + #color_map[next_id] = 'tab:red' validated_arrays[op.name] = op next_id = op.command_buffer.plot_graph( - validated_arrays, G, node_map, color_map, next_id, opnode) + validated_arrays, G, node_map, color_map, next_id, + opnode, save_op) elif isinstance(op, float) or isinstance(op, int): G.add_node(next_id) G.add_edge(opnode, next_id) node_map[next_id] = op next_id += 1 if parent is None: - pos = graphviz_layout(G, prog="dot") + pos = graphviz_layout(G, prog='dot') color_map_list = [color_map.get(node, 'tab:blue') for node in G] nx.draw(G, pos, labels=node_map, node_color=color_map_list, node_size=600, font_size=10) diff --git a/pyproject/ccs.py b/pyproject/ccs.py index d5914d4..f802cb0 100644 --- a/pyproject/ccs.py +++ b/pyproject/ccs.py @@ -3,6 +3,7 @@ from pyccs import Server from pyproject import array +debug = False server = None client_id = 0 next_name = 0 @@ -12,6 +13,18 @@ INV_OPCODES = {v: k for k, v in OPCODES.items()} +def enable_debug(): + global debug + debug = True + +def disable_debug(): + global debug + debug = False + +def is_debug(): + global debug + return debug + def get_name(): global next_name curr_name = next_name @@ -40,18 +53,19 @@ def send_command_async(handler, msg): server.send_request(handler, 0, msg) def connect(server_ip, server_port): - global server, client_id - server = Server(server_ip, server_port) - server.connect() - client_id = send_command(Handlers.connection_handler, "") - atexit.register(disconnect) + global server, client_id, debug + if not debug: + server = Server(server_ip, server_port) + server.connect() + client_id = send_command(Handlers.connection_handler, "") + atexit.register(disconnect) def disconnect(): global client_id cmd = to_bytes(client_id, 'B') send_command_async(Handlers.disconnection_handler, cmd) -def get_creation_command(arr, name, buf=None): +def get_creation_command(arr, name, shape, buf=None): """ Generate array creation CCS command """ @@ -59,7 +73,7 @@ def get_creation_command(arr, name, buf=None): cmd += to_bytes(arr.ndim, 'I') cmd += to_bytes(buf is not None, '?') cmd += to_bytes(arr.init_value is not None, '?') - for s in arr.shape: + for s in shape: cmd += to_bytes(int(s), 'L') if buf is not None: cmd += buf diff --git a/src/server.hpp b/src/server.hpp index 27d773a..1cf6f1c 100644 --- a/src/server.hpp +++ b/src/server.hpp @@ -365,10 +365,20 @@ aum_array_t calculate(astnode* node, std::vector &metadata) [&](auto& x, auto& y) { using T = std::decay_t; using V = std::decay_t; - if constexpr(std::is_same_v && + if constexpr (std::is_same_v && std::is_same_v) CmiAbort("Matrix multiplication not yet implemented"); - else if constexpr((std::is_same_v || + else if constexpr (std::is_same_v && + std::is_same_v) + { + if (!node->operands[0]->store) + res = aum::inplace_dot_1(x, y); + else if (!node->operands[1]->store) + res = aum::inplace_dot_2(x, y); + else + res = aum::dot(x, y); + } + else if constexpr ((std::is_same_v || std::is_same_v) && std::is_same_v) { From e4bdeb99cfcb75bc266c76c086b18f0ad2c38f3e Mon Sep 17 00:00:00 2001 From: Aditya Bhosale Date: Tue, 7 Jun 2022 23:40:06 -0500 Subject: [PATCH 15/16] Update README --- README.rst | 104 +++++++++++++++++++++++++++++++++++++ docs/images/conj_ast.png | Bin 0 -> 59237 bytes docs/images/simple_ast.png | Bin 0 -> 72222 bytes 3 files changed, 104 insertions(+) create mode 100644 docs/images/conj_ast.png create mode 100644 docs/images/simple_ast.png diff --git a/README.rst b/README.rst index e69de29..9945ace 100644 --- a/README.rst +++ b/README.rst @@ -0,0 +1,104 @@ +PyProject +========= + +PyProject is a python interface to a C++ distributed array library +implemented using Charm++ [#charm]_. +PyProject uses a client-server model with a client-side python +interface and a Charm++ server on the backend. The client and server +are connected using CCS [#ccs]_. +The server maintains a symbol table of distributed arrays which +are then looked up for computation when a CCS message is +received. + +:code:`pyproject.array` +---------------------- + +.. highlight:: python + +:code:`pyproject.array.ndarray`, analogous to :code:`numpy.ndarray`, is a proxy +object that wraps the name of the corresponding array on the server. +We use a lazy evaluation scheme for array computations. +The array operations incrementally build an AST which is stored in a buffer in the +:code:`ndarray` object. This AST is encoded into a CCS message when +either the data from the array is requested on the frontend or +when the size of the AST grows beyond a user configurable +threshold. +The server side Charm++ program decodes the CCS message and +rebuilds the AST which is then executed. + +The lazy evaluation scheme reduces the number of CCS messages required to +be sent from the client to the server. +It also helps in reducing the number of temporary arrays created on the +server side by accessing the reference counts of the frontend arrays in +the python runtime. For example:: + + v = ndarray(1, 10, np.float64) + b = ndarray(1, 10, np.float64) + c = ndarray(1, 10, np.float64) + w = c + for i in range(2): + y = v + b + w + z = v - y + w = 2 * (c - z) + b + w.evaluate() + +The above code snippet generates the following AST. Nodes with labels +starting with the letter :code:`a` are arrays. Nodes with an operation +label that are colored blue are operations that generate a temporary +array. Nodes with an operation label that are colored red are operations +that generate arrays that are to be stored on the server side. +The red node labels also show the name of the resulting array. +Note that the arrays :code:`y`, :code:`z` and :code:`w` for the first iteration +of the loop are considered to be temporary because they are overwritten +in the next iteration. These operations will be executed inplace +on the server side. + +.. figure:: docs/images/simple_ast.png + :alt: simple_ast + + *AST generated by the above code snippet* + +Here's another example of a conjugate gradient solver:: + + def solve(A, b): + x = ndarray(1, 1000, np.float64) + r = b - A @ x + p = r.copy() + rsold = r @ r + + for i in range(1000): + Ap = A @ p + alpha = rsold / (p @ Ap) + + x = lg.axpy(alpha, p, x) + r = lg.axpy(alpha, Ap, r, multiplier=-1.) + + rsnew = r @ r + + if np.sqrt(rsnew.get()) < 1e-8: + print("Converged in %i iterations" % (i + 1)) + break + + p = lg.axpy(rsnew / rsold, p, r) + rsold = rsnew + + return x + +This generates the following AST, + +.. figure:: docs/images/conj_ast.png + :alt: conj_ast + + *AST generated by the conjugate gradient example* + +Here the green nodes are arrays that do not exist on the server when the AST is +sent, but will be created and stored as a result of an operation in the current +AST before being referenced. + + +References +---------- + +.. [#charm] Charm++ Documentation - https://charm.readthedocs.io/en/latest/ +.. [#ccs] CCS Documentation - https://charm.readthedocs.io/en/latest/converse/manual.html?converse-client-server-interface#converse-client-server-interface + diff --git a/docs/images/conj_ast.png b/docs/images/conj_ast.png new file mode 100644 index 0000000000000000000000000000000000000000..e20d740f689dc1cba243733f2aaf1059905b258f GIT binary patch literal 59237 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 Date: Thu, 16 Jun 2022 16:15:15 -0500 Subject: [PATCH 16/16] Rename library to charmtiles --- README.rst | 12 ++++++------ {pyproject => charmtiles}/__init__.py | 0 {pyproject => charmtiles}/array.py | 4 ++-- {pyproject => charmtiles}/ast.py | 8 ++++---- {pyproject => charmtiles}/ccs.py | 2 +- {pyproject => charmtiles}/linalg.py | 6 +++--- examples/conjugate_gradient.py | 6 +++--- examples/graph.py | 8 ++++---- examples/simple.py | 4 ++-- setup.py | 4 ++-- 10 files changed, 27 insertions(+), 27 deletions(-) rename {pyproject => charmtiles}/__init__.py (100%) rename {pyproject => charmtiles}/array.py (98%) rename {pyproject => charmtiles}/ast.py (95%) rename {pyproject => charmtiles}/ccs.py (98%) rename {pyproject => charmtiles}/linalg.py (75%) diff --git a/README.rst b/README.rst index 9945ace..9e4a5b9 100644 --- a/README.rst +++ b/README.rst @@ -1,21 +1,21 @@ -PyProject -========= +charmtiles +========== -PyProject is a python interface to a C++ distributed array library +:code:`charmtiles` is a python interface to a C++ distributed array library implemented using Charm++ [#charm]_. -PyProject uses a client-server model with a client-side python +charmtiles uses a client-server model with a client-side python interface and a Charm++ server on the backend. The client and server are connected using CCS [#ccs]_. The server maintains a symbol table of distributed arrays which are then looked up for computation when a CCS message is received. -:code:`pyproject.array` +:code:`charmtiles.array` ---------------------- .. highlight:: python -:code:`pyproject.array.ndarray`, analogous to :code:`numpy.ndarray`, is a proxy +:code:`charmtiles.array.ndarray`, analogous to :code:`numpy.ndarray`, is a proxy object that wraps the name of the corresponding array on the server. We use a lazy evaluation scheme for array computations. The array operations incrementally build an AST which is stored in a buffer in the diff --git a/pyproject/__init__.py b/charmtiles/__init__.py similarity index 100% rename from pyproject/__init__.py rename to charmtiles/__init__.py diff --git a/pyproject/array.py b/charmtiles/array.py similarity index 98% rename from pyproject/array.py rename to charmtiles/array.py index f7037d8..c961004 100644 --- a/pyproject/array.py +++ b/charmtiles/array.py @@ -1,8 +1,8 @@ import sys import warnings import numpy as np -from pyproject.ast import get_max_depth, ASTNode -from pyproject.ccs import to_bytes, from_bytes, send_command_raw, send_command, \ +from charmtiles.ast import get_max_depth, ASTNode +from charmtiles.ccs import to_bytes, from_bytes, send_command_raw, send_command, \ send_command_async, connect, get_creation_command, \ get_name, get_fetch_command, Handlers, OPCODES, is_debug diff --git a/pyproject/ast.py b/charmtiles/ast.py similarity index 95% rename from pyproject/ast.py rename to charmtiles/ast.py index 4d73195..622c1a5 100644 --- a/pyproject/ast.py +++ b/charmtiles/ast.py @@ -3,7 +3,7 @@ import matplotlib.pyplot as plt from ctypes import c_long from networkx.drawing.nx_pydot import graphviz_layout -from pyproject.ccs import OPCODES, INV_OPCODES, to_bytes +from charmtiles.ccs import OPCODES, INV_OPCODES, to_bytes max_depth = 10 @@ -21,7 +21,7 @@ def get_max_depth(): class ASTNode(object): def __init__(self, name, opcode, operands): - from pyproject.array import ndarray + from charmtiles.array import ndarray # contains opcode, operands # operands are ndarrays self.name = name @@ -34,7 +34,7 @@ def __init__(self, name, opcode, operands): self.depth = max(self.depth, 1 + op.command_buffer.depth) def get_command(self, validated_arrays, save=True): - from pyproject.array import ndarray + from charmtiles.array import ndarray if self.opcode == 0: cmd = to_bytes(self.opcode, 'L') cmd += to_bytes(False, '?') @@ -70,7 +70,7 @@ def get_command(self, validated_arrays, save=True): def plot_graph(self, validated_arrays={}, G=None, node_map={}, color_map={}, next_id=0, parent=None, save=True): - from pyproject.array import ndarray + from charmtiles.array import ndarray if G is None: G = nx.Graph() if self.opcode == 0: diff --git a/pyproject/ccs.py b/charmtiles/ccs.py similarity index 98% rename from pyproject/ccs.py rename to charmtiles/ccs.py index f802cb0..3063d79 100644 --- a/pyproject/ccs.py +++ b/charmtiles/ccs.py @@ -1,7 +1,7 @@ import struct import atexit from pyccs import Server -from pyproject import array +from charmtiles import array debug = False server = None diff --git a/pyproject/linalg.py b/charmtiles/linalg.py similarity index 75% rename from pyproject/linalg.py rename to charmtiles/linalg.py index c5d1e9b..5449a69 100644 --- a/pyproject/linalg.py +++ b/charmtiles/linalg.py @@ -2,9 +2,9 @@ import struct import numpy as np from pyccs import Server -from pyproject.ccs import OPCODES, get_name, send_command, Handlers -from pyproject.array import create_ndarray -from pyproject.ast import ASTNode +from charmtiles.ccs import OPCODES, get_name, send_command, Handlers +from charmtiles.array import create_ndarray +from charmtiles.ast import ASTNode def axpy(a, x, y, multiplier=None): diff --git a/examples/conjugate_gradient.py b/examples/conjugate_gradient.py index e0c7c9a..6eac734 100644 --- a/examples/conjugate_gradient.py +++ b/examples/conjugate_gradient.py @@ -1,6 +1,6 @@ -from pyproject.array import connect, ndarray -import pyproject.linalg as lg -from pyproject.ccs import enable_debug +from charmtiles.array import connect, ndarray +import charmtiles.linalg as lg +from charmtiles.ccs import enable_debug import numpy as np import time diff --git a/examples/graph.py b/examples/graph.py index 6054c62..bd061b5 100644 --- a/examples/graph.py +++ b/examples/graph.py @@ -1,7 +1,7 @@ -from pyproject.array import connect, ndarray -from pyproject.ast import set_max_depth -from pyproject.ccs import enable_debug -import pyproject.linalg as lg +from charmtiles.array import connect, ndarray +from charmtiles.ast import set_max_depth +from charmtiles.ccs import enable_debug +import charmtiles.linalg as lg import numpy as np #enable_debug() diff --git a/examples/simple.py b/examples/simple.py index 2fc983a..463874b 100644 --- a/examples/simple.py +++ b/examples/simple.py @@ -1,5 +1,5 @@ -from pyproject.array import connect, ndarray -import pyproject.linalg as lg +from charmtiles.array import connect, ndarray +import charmtiles.linalg as lg import numpy as np def f(): diff --git a/setup.py b/setup.py index b521790..347b391 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ def get_version(): data = {} - fname = os.path.join('pyproject', '__init__.py') + fname = os.path.join('charmtiles', '__init__.py') exec(compile(open(fname).read(), fname, 'exec'), data) return data.get('__version__') @@ -41,7 +41,7 @@ def compile_server(): compile_server() setup( - name='pyproject', + name='charmtiles', version=get_version(), author='Aditya Bhosale', author_email='adityapb1546@gmail.com',