diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..4f2cf2e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,42 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "(gdb) 启动", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/example/example.out", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}/example", + "environment": [{ + "name": "PATH", + "value": "/opt/conda/envs/py38/bin" + }], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "为 gdb 启用整齐打印", + "text": "-enable-pretty-printing", + "ignoreFailures": true + }, + { + "description": "将反汇编风格设置为 Intel", + "text": "-gdb-set disassembly-flavor intel", + "ignoreFailures": true + } + ] + }, + { + "name": "Python: Current File", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/example/Makefile b/example/Makefile index 024b206..59cc1a4 100644 --- a/example/Makefile +++ b/example/Makefile @@ -2,7 +2,7 @@ CC = c++ # Compiler flags -CFLAGS = -O3 -Wall -std=c++17 -fPIC +CFLAGS = -g -Wall -std=c++17 -fPIC CFLAGS = -I../src/cpp CFLAGS += $(shell python3-config --cflags --ldflags --embed) CFLAGS += $(shell python3 -m pybind11 --includes) diff --git a/example/main.cpp b/example/main.cpp index e055f63..8a3b51c 100644 --- a/example/main.cpp +++ b/example/main.cpp @@ -1,6 +1,7 @@ #include #include #include +#define PYBIND11_DETAILED_ERROR_MESSAGES #include "PyCXpress.hpp" #include "Utils.hpp" @@ -8,31 +9,32 @@ namespace pcx = PyCXpress; void show_test(pcx::PythonInterpreter &python) { - std::vector data(6); - for (size_t i = 0; i < 6; i++) { + std::vector data(12); + for (size_t i = 0; i < 12; i++) { data[i] = i; } - memcpy(python.set_buffer("input_a", {3, 2}), data.data(), - data.size() * sizeof(float)); - memcpy(python.set_buffer("input_b", {3, 2}), data.data(), - data.size() * sizeof(float)); + std::vector shape = {3, 4}; + memcpy(python.set_buffer("data_to_be_reshaped", {12}), data.data(), + data.size() * sizeof(double)); + memcpy(python.set_buffer("new_2d_shape", {2}), shape.data(), + shape.size() * sizeof(uint8_t)); python.run(); void *p = nullptr; - std::vector shape; - std::tie(p, shape) = python.get_buffer("output_a"); + std::vector new_shape; + std::tie(p, new_shape) = python.get_buffer("output_a"); std::cout << "output shape: "; - std::copy(shape.begin(), shape.end(), - std::ostream_iterator(std::cout, ", ")); + std::copy(new_shape.begin(), new_shape.end(), + std::ostream_iterator(std::cout, ", ")); std::cout << std::endl; - size_t size = - std::accumulate(shape.begin(), shape.end(), 1, std::multiplies()); + size_t size = std::accumulate(new_shape.begin(), new_shape.end(), 1, + std::multiplies()); std::cout << "output data: "; - std::copy((float *)p, (float *)p + size, + std::copy((double *)p, (double *)p + size, std::ostream_iterator(std::cout, ", ")); std::cout << std::endl; } diff --git a/example/model.py b/example/model.py index 1131818..8945554 100644 --- a/example/model.py +++ b/example/model.py @@ -1,20 +1,69 @@ +import os +os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' import logging logging.basicConfig(level=logging.DEBUG) from pathlib import Path import sys +import numpy as np sys.path.append(str(Path(__file__).parent/".."/"src"/"python")) -from PyCXpress import debug_array -from PyCXpress import InputDataSet, OutputDataSet +from PyCXpress import TensorMeta, ModelAnnotationCreator, ModelAnnotationType, ModelRuntimeType +from PyCXpress import convert_to_spec_tuple from contextlib import nullcontext -def show(a): - debug_array(a) +def show(a: np.array): + logging.info(f"array data type: {a.dtype}") + logging.info(f"array data shape: {a.shape}") + logging.info(f"array data: ") + logging.info(a) + +InputFields = dict( + data_to_be_reshaped=TensorMeta(dtype=np.float_, + shape=(100,), + ), + new_2d_shape=TensorMeta(dtype=np.uint8, + shape=-2,) +) + + +class InputDataSet(metaclass=ModelAnnotationCreator, fields=InputFields, type=ModelAnnotationType.Input, mode=ModelRuntimeType.EagerExecution): + pass + + +OutputFields = dict( + output_a=TensorMeta(dtype=np.float_, + shape=(10, 10),), +) + + +class OutputDataSet(metaclass=ModelAnnotationCreator, fields=OutputFields, type=ModelAnnotationType.Output, mode=ModelRuntimeType.EagerExecution): + pass + def init(): - return InputDataSet(), OutputDataSet() + return InputDataSet(), OutputDataSet(), tuple((*convert_to_spec_tuple(InputFields.values()), *convert_to_spec_tuple(OutputFields.values()))), tuple(OutputFields.keys()) def model(input: InputDataSet, output: OutputDataSet): with nullcontext(): - output.output_a = input.input_a + input.input_b \ No newline at end of file + # print(input.data_to_be_reshaped) + # print(input.new_2d_shape) + output.output_a = input.data_to_be_reshaped.reshape(input.new_2d_shape) + # print(output.output_a) + +def main(): + input_data, output_data, spec = init() + print(spec) + + input_data.set_buffer_value("data_to_be_reshaped", np.arange(12, dtype=np.float_)) + print(input_data.data_to_be_reshaped) + input_data.set_buffer_value("new_2d_shape", np.array([3, 4]).astype(np.uint8)) + print(input_data.new_2d_shape) + output_data.set_buffer_value("output_a", np.arange(12)*0) + + model(input_data, output_data) + print(output_data.output_a) + print(output_data.get_buffer_shape("output_a")) + +if __name__ == "__main__": + main() diff --git a/src/cpp/PyCXpress.hpp b/src/cpp/PyCXpress.hpp index 7d62966..59a515d 100644 --- a/src/cpp/PyCXpress.hpp +++ b/src/cpp/PyCXpress.hpp @@ -37,12 +37,39 @@ class Buffer { m_data = new Bytes[m_size]; m_length = size; - if (data_type == "bool_") { + if (data_type == "bool") { m_converter = __to_array; m_length /= sizeof(bool); - } else if (data_type == "float_") { + } else if (data_type == "int8_t") { + m_converter = __to_array; + m_length /= sizeof(int8_t); + } else if (data_type == "int16_t") { + m_converter = __to_array; + m_length /= sizeof(int16_t); + } else if (data_type == "int32_t") { + m_converter = __to_array; + m_length /= sizeof(int32_t); + } else if (data_type == "int64_t") { + m_converter = __to_array; + m_length /= sizeof(int64_t); + } else if (data_type == "uint8_t") { + m_converter = __to_array; + m_length /= sizeof(uint8_t); + } else if (data_type == "uint16_t") { + m_converter = __to_array; + m_length /= sizeof(uint16_t); + } else if (data_type == "uint32_t") { + m_converter = __to_array; + m_length /= sizeof(uint32_t); + } else if (data_type == "uint64_t") { + m_converter = __to_array; + m_length /= sizeof(uint64_t); + } else if (data_type == "float") { m_converter = __to_array; m_length /= sizeof(float); + } else if (data_type == "double") { + m_converter = __to_array; + m_length /= sizeof(double); } else { throw NotImplementedError(data_type); } @@ -98,31 +125,34 @@ class PythonInterpreter { const std::vector &shape) { auto &buf = m_buffers[name]; void *p = buf.set(shape); - m_py_input.attr("set")(name, buf.get()); + m_py_input.attr("set_buffer_value")(name, buf.get()); return p; } std::pair> get_buffer(const std::string &name) { - auto &array = m_buffers[name].get(); - return std::make_pair( - array.request().ptr, - std::vector(array.shape(), array.shape() + array.ndim())); + auto &array = m_buffers[name].get(); + auto pShape = m_output_buffer_sizes.find(name); + if (pShape == m_output_buffer_sizes.end()) { + return std::make_pair( + array.request().ptr, + std::vector(array.shape(), + array.shape() + array.ndim())); + } else { + return std::make_pair(array.request().ptr, pShape->second); + } } void run() { - auto &buf = m_buffers["output_a"]; - buf.reset(); - m_py_output.attr("set_buffer")("output_a", buf.get()); - p_pkg->attr("model")(m_py_input, m_py_output); - py::tuple py_shape = m_py_output.attr("get_shape")("output_a"); - auto &shape = m_output_buffer_sizes["output_a"]; - shape.clear(); - for (auto d = py_shape.begin(); d != py_shape.end(); d++) { - shape.push_back(d->cast()); + for (auto &kv : m_output_buffer_sizes) { + kv.second.clear(); + py::tuple shape = m_py_output.attr("get_buffer_shape")(kv.first); + + for (auto &d : shape) { + kv.second.push_back(d.cast()); + } } - set_buffer("output_a", shape); } void show_buffer(const std::string &name) { @@ -135,19 +165,29 @@ class PythonInterpreter { const char *const *argv, bool add_program_dir_to_path) { py::initialize_interpreter(true, 0, nullptr, true); - m_buffers.insert(std::make_pair("input_a", Buffer{1000, "float_"})); - m_buffers.insert(std::make_pair("input_b", Buffer{1000, "float_"})); - m_buffers.insert(std::make_pair("output_a", Buffer{1000, "float_"})); - - p_pkg = std::make_unique(py::module_::import("model")); - py::print(p_pkg->attr("__file__")); - m_py_input = p_pkg->attr("InputDataSet")(); - m_py_output = p_pkg->attr("OutputDataSet")(); + py::tuple spec, output_fields; + std::tie(m_py_input, m_py_output, spec, output_fields) = + p_pkg->attr("init")() + .cast< + std::tuple>(); + + for (auto d = spec.begin(); d != spec.end(); d++) { + auto meta = d->cast(); + m_buffers.insert(std::make_pair( + meta[0].cast(), + Buffer{meta[2].cast(), meta[1].cast()})); + } - m_py_output.attr("set_buffer")("output_a", m_buffers["output_a"].get()); + for (auto d = output_fields.begin(); d != output_fields.end(); d++) { + const auto name = d->cast(); + m_output_buffer_sizes[name] = {}; + auto &buf = m_buffers[name]; + buf.reset(); + m_py_output.attr("set_buffer_value")(name, buf.get()); + } } void finalize() { diff --git a/src/python/PyCXpress.py b/src/python/PyCXpress.py index ecd7d0f..03d6f50 100644 --- a/src/python/PyCXpress.py +++ b/src/python/PyCXpress.py @@ -1,60 +1,160 @@ -import os -os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' import logging -from contextlib import nullcontext logger = logging.getLogger(__name__) -import numpy as np - -import tensorflow as tf -def debug_array(a: np.array): - logger.info(f"array data type: {a.dtype}") - logger.info(f"array data shape: {a.shape}") - logger.info(f"array data: ") - logger.info(a) +import numpy as np +# import tensorflow as tf +from typing import List, Tuple, Iterable, Callable, Dict, Optional, Union -class InputDataSet(object): - def __init__(self): - self.data = {} +from collections import namedtuple +from dataclasses import dataclass +import numpy as np +from numpy.typing import DTypeLike +from enum import Enum, auto + + +def get_c_type(t: DTypeLike) -> (str, int): + dtype = np.dtype(t) + relation = {np.dtype('bool'): 'bool', + np.dtype('int8'): 'int8_t', + np.dtype('int16'): 'int16_t', + np.dtype('int32'): 'int32_t', + np.dtype('int64'): 'int64_t', + np.dtype('uint8'): 'uint8_t', + np.dtype('uint16'): 'uint16_t', + np.dtype('uint32'): 'uint32_t', + np.dtype('uint64'): 'uint64_t', + np.dtype('float32'): 'float', + np.dtype('float64'): 'double'} + return relation.get(dtype, "char"), dtype.itemsize or 1 + + +@dataclass +class TensorMeta: + dtype: DTypeLike # the data type similar to np.int_ + shape: Union[int, Iterable[int], Callable[..., Union[int, Iterable[int]]]] # the maximal size of each dimension + name: Optional[str] = None + doc: Optional[str] = None + + def to_dict(self, *args, **kwargs) -> dict: + assert self.name is not None + + max_size = self.shape + if isinstance(max_size, Callable): + max_size = max_size(*args, **kwargs) + max_size = tuple(max_size) if isinstance(self.shape, Iterable) else (max_size,) + + dtype, itemsize = get_c_type(self.dtype) + + return { + "name": self.name, + "dtype": dtype, + "shape": tuple(round(-i) if i<0 else None for i in max_size), + "buffer_size": np.prod([round(abs(i)) for i in max_size]) * itemsize, + "doc": f"" if self.doc is None else self.doc + } + + def setdefault(self, name: str): + if self.name is None: + self.name = name + return self.name + + +class ModelAnnotationType(Enum): + Input = auto() + Output = auto() + Operator = auto() + HyperParams = auto() + +class ModelRuntimeType(Enum): + GraphExecution = auto() + EagerExecution = auto() + OfflineExecution = auto() + +@dataclass +class TensorWithShape: + data: Optional[np.array] = None + shape: Optional[Tuple] = None + +class ModelAnnotationCreator(type): + def __new__(mcs, name, bases, attrs, fields: Dict[str, TensorMeta], type: ModelAnnotationType, mode: ModelRuntimeType): + if type == ModelAnnotationType.Input: + generate_property = mcs.generate_input_property + elif type == ModelAnnotationType.Output: + generate_property = mcs.generate_output_property + else: + raise NotImplementedError() + + for field_name, field_meta in fields.items(): + field_meta.setdefault(field_name) + attrs[field_name] = generate_property(field_meta) + + get_buffer_shape, set_buffer_value, init_func = mcs.general_funcs(name, [field_meta.name for field_meta in fields.values()]) + + + attrs["__init__"] = init_func + attrs["set_buffer_value"] = set_buffer_value + if type == ModelAnnotationType.Output: + attrs["get_buffer_shape"] = get_buffer_shape + attrs.setdefault("__slots__", []).append("__buffer_data__") + + + return super().__new__(mcs, name, bases, attrs) + + @staticmethod + def general_funcs(name: str, field_names: list): + def get_buffer_shape(self, name: str): + buffer = getattr(self.__buffer_data__, name) + return buffer.shape + + def set_buffer_value(self, name: str, value): + buffer = getattr(self.__buffer_data__, name) + buffer.data = value + + def init_func(self): + _BufferData_ = namedtuple("_BufferData_", field_names) + self.__buffer_data__ = _BufferData_(*tuple(TensorWithShape() for _ in field_names)) + + return get_buffer_shape, set_buffer_value, init_func + + @staticmethod + def generate_input_property(field: TensorMeta): + def get_func(self): + return getattr(self.__buffer_data__, field.name).data - def set(self, name:str, x: np.array): - self.data[name] = x + def set_func(*_): + raise AssertionError("Not supported for input tensor") - @property - def input_a(self): - return tf.Variable(self.data['input_a'], name="input_a") + def del_func(_): + raise AssertionError("Not supported for input tensor") - @property - def input_b(self): - return tf.Variable(self.data['input_b'], name="input_b") + return property(fget=get_func, fset=set_func, fdel=del_func, doc=field.doc) -class OutputDataSet(object): - def __init__(self): - self.data = {} + @staticmethod + def generate_output_property(field: TensorMeta): + def get_func(self): + logger.warning(f"Only read the data field {field.name} in debugging mode") + buffer = getattr(self.__buffer_data__, field.name) + return buffer.data[:np.prod(buffer.shape)].reshape(buffer.shape) - def set_buffer(self, name: str, x: np.array): - assert x.ndim == 1 - self.data[name] = {"data": x} + def set_func(self, data): + buffer = getattr(self.__buffer_data__, field.name) + buffer.shape = data.shape + buffer.data[:np.prod(data.shape)] = data.flatten() - def get_shape(self, name: str): - return self.data[name]['shape'] + def del_func(_): + raise AssertionError("Not supported for output tensor") - @property - def output_a(self, x: tf.Variable): - raise NotImplementedError + return property(fget=get_func, fset=set_func, fdel=del_func, doc=field.doc) - @output_a.setter - def output_a(self, x: tf.Variable): - buffer = self.data['output_a'] - buffer['shape'] = x.shape - buffer['data'][:np.prod(x.shape)] = x.numpy().flatten() +def convert_to_spec_tuple(fields: Iterable[TensorMeta]): + return tuple((v["name"], v["dtype"], v["buffer_size"]) for v in [v.to_dict() for v in fields]) def main(): pass if __name__ == "__main__": - main() \ No newline at end of file + main()