Browse Source

Python pnnx with pnnx binary (#5067)

tags/20240102
Zhenyu ZHAO GitHub 2 years ago
parent
commit
ef29bbec49
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1238 additions and 0 deletions
  1. +1
    -0
      .gitignore
  2. +199
    -0
      tools/pnnx/python/README.md
  3. +41
    -0
      tools/pnnx/python/examples/convert.py
  4. +41
    -0
      tools/pnnx/python/examples/export.py
  5. +33
    -0
      tools/pnnx/python/pnnx/__init__.py
  6. +16
    -0
      tools/pnnx/python/pnnx/utils/__init__.py
  7. +94
    -0
      tools/pnnx/python/pnnx/utils/convert.py
  8. +169
    -0
      tools/pnnx/python/pnnx/utils/export.py
  9. +91
    -0
      tools/pnnx/python/pnnx/utils/utils.py
  10. +1
    -0
      tools/pnnx/python/requirements.txt
  11. +45
    -0
      tools/pnnx/python/setup.py.i
  12. +69
    -0
      tools/pnnx/python/tests/test_convert.py
  13. +43
    -0
      tools/pnnx/python/tests/test_dynamicinput_convert.py
  14. +40
    -0
      tools/pnnx/python/tests/test_dynamicinput_export.py
  15. +67
    -0
      tools/pnnx/python/tests/test_export.py
  16. +63
    -0
      tools/pnnx/python/tests/test_naiveinput_convert.py
  17. +60
    -0
      tools/pnnx/python/tests/test_naiveinput_export.py
  18. +165
    -0
      tools/pnnx/setup.py

+ 1
- 0
.gitignore View File

@@ -54,6 +54,7 @@ __pycache__
*.pyd
*.egg-info/
python/setup.py
tools/pnnx/python/setup.py

# Clangd
.cache/


+ 199
- 0
tools/pnnx/python/README.md View File

@@ -0,0 +1,199 @@
# pnnx
python wrapper of pnnx, only support python 3.7+ now.

Install from pip
==================

pnnx is available as wheel packages for macOS, Windows and Linux distributions, you can install with pip:

```
pip install pnnx
```

# Build & Install from source

## Prerequisites

**On Unix (Linux, OS X)**

* A compiler with C++14 support
* CMake >= 3.4

**On Mac**

* A compiler with C++14 support
* CMake >= 3.4

**On Windows**

* Visual Studio 2015 or higher
* CMake >= 3.4

## Build & install
1. clone ncnn.
```bash
git clone https://github.com/Tencent/ncnn.git
```
2. install pytorch

install pytorch according to https://pytorch.org/ . Anaconda is strongly recommended for example:
```bash
conda install pytorch
```
3. install
```bash
cd /pathto/ncnntools/pnnx
python setup.py install
```

> **Note:**
> If torchvision and pnnx2onnx are needed, you can set the following environment variables before 'python setup.py install' to enable them. e.g. on ubuntu:
>
> ```
> export TORCHVISION_INSTALL_DIR="/project/torchvision"
> export PROTOBUF_INCLUDE_DIR="/project/protobuf/include"
> export PROTOBUF_LIBRARIES="/project/protobuf/lib64/libprotobuf.a"
> export PROTOBUF_PROTOC_EXECUTABLE="/project/protobuf/bin/protoc"
> ```
>
> To do these, you must install Torchvision and Protobuf first.


## Tests
```bash
cd /pathto/ncnn/tools/pnnx
pytest python/tests/
```

## Usage
1. export model to pnnx
```python
import torch
import torchvision.models as models
import pnnx

net = models.resnet18(pretrained=True)
x = torch.rand(1, 3, 224, 224)

# You could try disabling checking when torch tracing raises error
# mod = pnnx.export(net, "resnet18", x, check_trace=False)
mod = pnnx.export(net, "resnet18", x)
```

2. convert existing model to pnnx
```python
import pnnx

pnnx.convert("resnet18.pt", [1,3,224,224], "f32")
```

## API Reference
1. pnnx.export

`model` (torch.nn.Model): model to be exported.

`filename` (str): the file name.

`inputs` (torch.Tensor of list of torch.Tensor) expected inputs of the model.

`input_shapes` (Optional, list of int or list of list with int type inside) shapes of model inputs.
It is used to resolve tensor shapes in model graph. for example, [1,3,224,224] for the model with only
1 input, [[1,3,224,224],[1,3,224,224]] for the model that have 2 inputs.

`input_shapes2` (Optional, list of int or list of list with int type inside) shapes of alternative model inputs,
the format is identical to `input_shapes`. Usually, it is used with input_shapes to resolve dynamic shape (-1)
in model graph.

`input_types` (Optional, str or list of str) types of model inputs, it should have the same length with `input_shapes`.
for example, "f32" for the model with only 1 input, ["f32", "f32"] for the model that have 2 inputs.

| typename | torch type |
|:--------:|:--------------------------------|
| f32 | torch.float32 or torch.float |
| f64 | torch.float64 or torch.double |
| f16 | torch.float16 or torch.half |
| u8 | torch.uint8 |
| i8 | torch.int8 |
| i16 | torch.int16 or torch.short |
| i32 | torch.int32 or torch.int |
| i64 | torch.int64 or torch.long |
| c32 | torch.complex32 |
| c64 | torch.complex64 |
| c128 | torch.complex128 |

`input_types2` (Optional, str or list of str) types of alternative model inputs.

`device` (Optional, str, default="cpu") device type for the input in TorchScript model, cpu or gpu.

`customop_modules` (Optional, str or list of str) list of Torch extensions (dynamic library) for custom operators.
For example, "/home/nihui/.cache/torch_extensions/fused/fused.so" or
["/home/nihui/.cache/torch_extensions/fused/fused.so",...].

`module_operators` (Optional, str or list of str) list of modules to keep as one big operator.
for example, "models.common.Focus" or ["models.common.Focus","models.yolo.Detect"].

`optlevel` (Optional, int, default=2) graph optimization level

| option | optimization level |
|:--------:|:----------------------------------|
| 0 | do not apply optimization |
| 1 | do not apply optimization |
| 2 | optimization more for inference |

`pnnxparam` (Optional, str, default="*.pnnx.param", * is the model name): PNNX graph definition file.

`pnnxbin` (Optional, str, default="*.pnnx.bin"): PNNX model weight.

`pnnxpy` (Optional, str, default="*_pnnx.py"): PyTorch script for inference, including model construction
and weight initialization code.

`pnnxonnx` (Optional, str, default="*.pnnx.onnx"): PNNX model in onnx format.

`ncnnparam` (Optional, str, default="*.ncnn.param"): ncnn graph definition.

`ncnnbin` (Optional, str, default="*.ncnn.bin"): ncnn model weight.

`ncnnpy` (Optional, str, default="*_ncnn.py"): pyncnn script for inference.

2. pnnx.convert

`ptpath` (str): torchscript model to be converted.

`input_shapes` (list of int or list of list with int type inside) shapes of model inputs.
It is used to resolve tensor shapes in model graph. for example, [1,3,224,224] for the model with only
1 input, [[1,3,224,224],[1,3,224,224]] for the model that have 2 inputs.

`input_types` (str or list of str) types of model inputs, it should have the same length with `input_shapes`.
for example, "f32" for the model with only 1 input, ["f32", "f32"] for the model that have 2 inputs.

`input_shapes2` (Optional, list of int or list of list with int type inside) shapes of alternative model inputs,
the format is identical to `input_shapes`. Usually, it is used with input_shapes to resolve dynamic shape (-1)
in model graph.

`input_types2` (Optional, str or list of str) types of alternative model inputs.

`device` (Optional, str, default="cpu") device type for the input in TorchScript model, cpu or gpu.

`customop_modules` (Optional, str or list of str) list of Torch extensions (dynamic library) for custom operators.
For example, "/home/nihui/.cache/torch_extensions/fused/fused.so" or
["/home/nihui/.cache/torch_extensions/fused/fused.so",...].

`module_operators` (Optional, str or list of str) list of modules to keep as one big operator.
for example, "models.common.Focus" or ["models.common.Focus","models.yolo.Detect"].

`optlevel` (Optional, int, default=2) graph optimization level

`pnnxparam` (Optional, str, default="*.pnnx.param", * is the model name): PNNX graph definition file.

`pnnxbin` (Optional, str, default="*.pnnx.bin"): PNNX model weight.

`pnnxpy` (Optional, str, default="*_pnnx.py"): PyTorch script for inference, including model construction
and weight initialization code.

`pnnxonnx` (Optional, str, default="*.pnnx.onnx"): PNNX model in onnx format.

`ncnnparam` (Optional, str, default="*.ncnn.param"): ncnn graph definition.

`ncnnbin` (Optional, str, default="*.ncnn.bin"): ncnn model weight.

`ncnnpy` (Optional, str, default="*_ncnn.py"): pyncnn script for inference.

+ 41
- 0
tools/pnnx/python/examples/convert.py View File

@@ -0,0 +1,41 @@
# Tencent is pleased to support the open source community by making ncnn available.
#
# Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
#
# Licensed under the BSD 3-Clause License (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
#
# https://opensource.org/licenses/BSD-3-Clause
#
# Unless required by applicable law or agreed to in writing, software distributed
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.

import pnnx

import torch
import torch.nn as nn
import torch.nn.functional as F

class Model(nn.Module):
def __init__(self):
super(Model, self).__init__()

def forward(self, x):
x = F.relu(x)
return x

if __name__ == "__main__":
net = Model()
net.eval()

torch.manual_seed(0)
x = torch.rand(1, 16)

a0 = net(x)

mod = torch.jit.trace(net, x)
mod.save("test_F_relu.pt")

pnnx.convert("test_F_relu.pt", [1, 16], "f32")

+ 41
- 0
tools/pnnx/python/examples/export.py View File

@@ -0,0 +1,41 @@
# Tencent is pleased to support the open source community by making ncnn available.
#
# Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
#
# Licensed under the BSD 3-Clause License (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
#
# https://opensource.org/licenses/BSD-3-Clause
#
# Unless required by applicable law or agreed to in writing, software distributed
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.

import pnnx

import torch
import torch.nn as nn
import torch.nn.functional as F

class Model(nn.Module):
def __init__(self):
super(Model, self).__init__()

def forward(self, x, y, z, w):
x = F.relu(x)
y = F.relu(y)
z = F.relu(z)
w = F.relu(w)
return x, y, z, w

if __name__ == "__main__":
net = Model()

torch.manual_seed(0)
x = torch.rand(1, 16)
y = torch.rand(12, 2, 16)
z = torch.rand(1, 3, 12, 16)
w = torch.rand(1, 5, 7, 9, 11)

pnnx.export(net, "test_F_relu", (x, y, z, w))

+ 33
- 0
tools/pnnx/python/pnnx/__init__.py View File

@@ -0,0 +1,33 @@
# Tencent is pleased to support the open source community by making ncnn available.
#
# Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
#
# Licensed under the BSD 3-Clause License (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
#
# https://opensource.org/licenses/BSD-3-Clause
#
# Unless required by applicable law or agreed to in writing, software distributed
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.

import os
import platform
EXEC_DIR_PATH = os.path.dirname(os.path.abspath(__file__))
if platform.system() == 'Linux' or platform.system() == "Darwin":
EXEC_PATH = EXEC_DIR_PATH + "/pnnx"
elif platform.system() == "Windows":
EXEC_PATH = EXEC_DIR_PATH + "/pnnx.exe"
else:
raise Exception("Unsupported platform for pnnx.")

from .utils.export import export
from .utils.convert import convert

try:
import importlib.metadata
__version__ = importlib.metadata.version("pnnx")
except:
pass


+ 16
- 0
tools/pnnx/python/pnnx/utils/__init__.py View File

@@ -0,0 +1,16 @@
# Tencent is pleased to support the open source community by making ncnn available.
#
# Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
#
# Licensed under the BSD 3-Clause License (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
#
# https://opensource.org/licenses/BSD-3-Clause
#
# Unless required by applicable law or agreed to in writing, software distributed
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.

from .export import export
from .convert import convert

+ 94
- 0
tools/pnnx/python/pnnx/utils/convert.py View File

@@ -0,0 +1,94 @@
# Tencent is pleased to support the open source community by making ncnn available.
#
# Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
#
# Licensed under the BSD 3-Clause License (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
#
# https://opensource.org/licenses/BSD-3-Clause
#
# Unless required by applicable law or agreed to in writing, software distributed
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.

import os
from .utils import check_type, generate_inputs_arg, str_in_list_to_str
import subprocess
from .. import EXEC_PATH

def convert(ptpath, input_shapes, input_types, input_shapes2 = None,
input_types2 = None, device = None, customop = None,
moduleop = None, optlevel = None, pnnxparam = None,
pnnxbin = None, pnnxpy = None, pnnxonnx = None, ncnnparam = None,
ncnnbin = None, ncnnpy = None):

check_type(ptpath, "modelname", [str], "str")
check_type(input_shapes, "input_shapes", [list], "list of list with int type inside")
check_type(input_types, "input_types", [str, list], "str or list of str")
check_type(input_shapes2, "input_shapes2", [list], "list of list with int type inside")
check_type(input_types2, "input_types2", [str, list], "str or list of str")
check_type(device, "device", [str], "str")
check_type(customop, "customop", [str, list], "str or list of str")
check_type(moduleop, "moduleop", [str, list], "str or list of str")
check_type(optlevel, "optlevel", [int], "int")

if input_shapes2 is None:
input_shapes2 = []
elif type(input_shapes2[0])!= list:
input_shapes2 = [input_shapes2]
if input_types2 is None:
input_types2 = []
elif type(input_types2) != list:
input_types2 = [input_types2]
if customop is None:
customop = []
elif type(customop) != list:
customop = [customop]
if moduleop is None:
moduleop = []
elif type(moduleop) != list:
moduleop = [moduleop]
if device is None:
device = "cpu"
if optlevel is None:
optlevel = 2

if type(input_shapes[0]) != list:
input_shapes = [input_shapes]
if type(input_types) != list:
input_types = [input_types]

if len(input_shapes) != len(input_types):
raise Exception("input_shapes should has the same length with input_types!")
if len(input_shapes2) != len(input_types2):
raise Exception("input_shapes2 should has the same length with input_types2!")

input_arg1 = generate_inputs_arg(input_shapes, input_types)

command_list = [EXEC_PATH, ptpath, "inputshape="+input_arg1,
"device="+device,
"optlevel="+str(optlevel)]
if not (len(input_shapes2) == 0):
input_arg2 = generate_inputs_arg(input_shapes2, input_types2)
command_list.append("inputshape2=" + input_arg2)
if not (len(customop) == 0):
command_list.append("customop=" + str_in_list_to_str(customop))
if not (len(moduleop) == 0):
command_list.append("moduleop=" + str_in_list_to_str(moduleop))
if not(pnnxparam is None):
command_list.append("pnnxparam="+pnnxparam)
if not(pnnxbin is None):
command_list.append("pnnxbin="+pnnxbin)
if not(pnnxpy is None):
command_list.append("pnnxpy="+pnnxpy)
if not(pnnxonnx is None):
command_list.append("pnnxonnx="+pnnxonnx)
if not(ncnnparam is None):
command_list.append("ncnnparam="+ncnnparam)
if not(ncnnbin is None):
command_list.append("ncnnbin="+ncnnbin)
if not(ncnnpy is None):
command_list.append("ncnnpy="+ncnnpy)
current_dir = os.getcwd()
subprocess.run(command_list, stdout=subprocess.PIPE, text=True, cwd=current_dir)

+ 169
- 0
tools/pnnx/python/pnnx/utils/export.py View File

@@ -0,0 +1,169 @@
# Tencent is pleased to support the open source community by making ncnn available.
#
# Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
#
# Licensed under the BSD 3-Clause License (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
#
# https://opensource.org/licenses/BSD-3-Clause
#
# Unless required by applicable law or agreed to in writing, software distributed
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.

import torch
import os
from .utils import check_type, get_shape_from_inputs, \
get_type_from_inputs, generate_inputs_arg, str_in_list_to_str
import subprocess
from .. import EXEC_PATH

def export(model, filename, inputs = None, input_shapes = None, input_shapes2 = None,
input_types = None, input_types2 = None, device = None, customop = None,
moduleop = None, optlevel = None, pnnxparam = None, pnnxbin = None,
pnnxpy = None, pnnxonnx = None, ncnnparam = None, ncnnbin = None, ncnnpy = None,
check_trace=True):
if (inputs is None) and (input_shapes is None):
raise Exception("inputs or input_shapes should be specified.")
if not (input_shapes is None) and (input_types is None):
raise Exception("when input_shapes is specified, then input_types should be specified correspondingly.")

check_type(filename, "filename", [str], "str")
check_type(inputs, "inputs", [torch.Tensor, tuple, list], "torch.Tensor or tuple/list of torch.Tensor")
check_type(input_shapes, "input_shapes", [list], "list of list with int type inside")
check_type(input_types, "input_types", [str, list], "str or list of str")
check_type(input_shapes2, "input_shapes2", [list], "list of list with int type inside")
check_type(input_types2, "input_types2", [str, list], "str or list of str")
check_type(device, "device", [str], "str")
check_type(customop, "customop", [str, list], "str or list of str")
check_type(moduleop, "moduleop", [str, list], "str or list of str")
check_type(optlevel, "optlevel", [int], "int")

if input_shapes2 is None:
input_shapes2 = []
elif type(input_shapes2[0])!= list:
input_shapes2 = [input_shapes2]
if input_types2 is None:
input_types2 = []
elif type(input_types2) != list:
input_types2 = [input_types2]
if customop is None:
customop = []
elif type(customop) != list:
customop = [customop]
if moduleop is None:
moduleop = []
elif type(moduleop) != list:
moduleop = [moduleop]
if optlevel is None:
optlevel = 2
if type(inputs) == torch.Tensor:
inputs = [inputs]

if not (inputs is None):
model.eval()
mod = torch.jit.trace(model, inputs, check_trace=check_trace)
mod.save(filename)
current_path = os.path.abspath(filename)

if device is None:
try:
devicename = str(next(model.parameters()).device)
if ("cpu" in devicename):
device = "cpu"
elif ("cuda" in devicename):
device = "gpu"
except: # model without parameters
device = "cpu"

if input_shapes is None:
input_shapes = get_shape_from_inputs(inputs)
input_types = get_type_from_inputs(inputs)
else:
if type(input_shapes[0]) != list:
input_shapes = [input_shapes]
if type(input_types) != list:
input_types = [input_types]

if len(input_shapes) != len(input_types):
raise Exception("input_shapes should has the same length with input_types!")
if len(input_shapes2) != len(input_types2):
raise Exception("input_shapes2 should has the same length with input_types2!")

input_arg1 = generate_inputs_arg(input_shapes, input_types)

command_list = [EXEC_PATH, current_path, "inputshape=" + input_arg1,
"device=" + device,
"optlevel=" + str(optlevel)]
if not (len(input_shapes2) == 0):
input_arg2 = generate_inputs_arg(input_shapes2, input_types2)
command_list.append("inputshape2=" + input_arg2)
if not (len(customop) == 0):
command_list.append("customop=" + str_in_list_to_str(customop))
if not (len(moduleop) == 0):
command_list.append("moduleop=" + str_in_list_to_str(moduleop))

if not (pnnxparam is None):
command_list.append("pnnxparam=" + pnnxparam)
if not (pnnxbin is None):
command_list.append("pnnxbin=" + pnnxbin)
if not (pnnxpy is None):
command_list.append("pnnxpy=" + pnnxpy)
if not (pnnxonnx is None):
command_list.append("pnnxonnx=" + pnnxonnx)
if not (ncnnparam is None):
command_list.append("ncnnparam=" + ncnnparam)
if not (ncnnbin is None):
command_list.append("ncnnbin=" + ncnnbin)
if not (ncnnpy is None):
command_list.append("ncnnpy=" + ncnnpy)
current_dir = os.getcwd()
subprocess.run(command_list, stdout=subprocess.PIPE, text=True, cwd=current_dir)

else: # use input_shapes and input_types
if (input_shapes is None) or (input_types is None):
raise Exception("input_shapes and input_types should be specified together.")
model.eval()
mod = torch.jit.trace(model, inputs, check_trace=check_trace)
mod.save(filename)
current_path = os.path.abspath(filename)

if device is None:
try:
devicename = str(next(model.parameters()).device)
if ("cpu" in devicename):
device = "cpu"
elif ("cuda" in devicename):
device = "gpu"
except: # model without parameters
device = "cpu"

input_arg1 = generate_inputs_arg(input_shapes, input_types)

command_list = [EXEC_PATH, current_path, "inputshape=" + input_arg1,
"device=" + device,
"optlevel=" + str(optlevel)]
if not (len(input_shapes2) == 0):
input_arg2 = generate_inputs_arg(input_shapes2, input_types2)
command_list.append("inputshape2=" + input_arg2)
if not (len(customop) == 0):
command_list.append("customop=" + str_in_list_to_str(customop))
if not (len(moduleop) == 0):
command_list.append("moduleop=" + str_in_list_to_str(moduleop))
if not (pnnxparam is None):
command_list.append("pnnxparam=" + pnnxparam)
if not (pnnxbin is None):
command_list.append("pnnxbin=" + pnnxbin)
if not (pnnxpy is None):
command_list.append("pnnxpy=" + pnnxpy)
if not (pnnxonnx is None):
command_list.append("pnnxonnx=" + pnnxonnx)
if not (ncnnparam is None):
command_list.append("ncnnparam=" + ncnnparam)
if not (ncnnbin is None):
command_list.append("ncnnbin=" + ncnnbin)
if not (ncnnpy is None):
command_list.append("ncnnpy=" + ncnnpy)
current_dir = os.getcwd()
subprocess.run(command_list, stdout=subprocess.PIPE, text=True, cwd=current_dir)

+ 91
- 0
tools/pnnx/python/pnnx/utils/utils.py View File

@@ -0,0 +1,91 @@
# Tencent is pleased to support the open source community by making ncnn available.
#
# Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
#
# Licensed under the BSD 3-Clause License (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
#
# https://opensource.org/licenses/BSD-3-Clause
#
# Unless required by applicable law or agreed to in writing, software distributed
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.

import torch

def check_type(data, dataname, types, typesname):
if not(data is None):
if (type(data) in types):
return True
else:
raise Exception(dataname + " should be "+ typesname + ".")
else:
return True

def get_shape_from_inputs(inputs):
shapes = []
for item in inputs:
sub_shapes = []
for l in item.shape:
sub_shapes.append(l)
shapes.append(sub_shapes)
return shapes

def input_torch_type_to_str(tensor):
if tensor.dtype == torch.float32 or tensor.dtype == torch.float:
return "f32"
if tensor.dtype == torch.float64 or tensor.dtype == torch.double:
return "f64"
if tensor.dtype == torch.float16 or tensor.dtype == torch.half:
return "f16"
if tensor.dtype == torch.uint8:
return "u8"
if tensor.dtype == torch.int8:
return "i8"
if tensor.dtype == torch.int16 or tensor.dtype == torch.short:
return "i16"
if tensor.dtype == torch.int32 or tensor.dtype == torch.int:
return "i32"
if tensor.dtype == torch.int64 or tensor.dtype == torch.long:
return "i64"
if tensor.dtype == torch.complex32:
return "c32"
if tensor.dtype == torch.complex64:
return "c64"
if tensor.dtype == torch.complex128:
return "c128"

return "f32"

def get_type_from_inputs(inputs):
types = []
for item in inputs:
types.append(input_torch_type_to_str(item))
return types

def generate_inputs_arg(inputs, input_shapes):
generated_arg = ""
for i in range(0, len(inputs) - 1):
generated_arg += "["
for j in range(0, len(inputs[i]) - 1):
generated_arg += str(inputs[i][j]) + ','
generated_arg += str(inputs[i][-1])
generated_arg += "]"
generated_arg += input_shapes[i]
generated_arg += ","
generated_arg += "["
for j in range(0, len(inputs[-1]) - 1):
generated_arg += str(inputs[-1][j]) + ','
generated_arg += str(inputs[-1][-1])
generated_arg += "]"
generated_arg += input_shapes[-1]
return generated_arg

def str_in_list_to_str(input_list):
generated_str = ""
for i in range(0, len(input_list) - 1):
generated_str += input_list[i] + ','
generated_str += input_list[-1]
return generated_str


+ 1
- 0
tools/pnnx/python/requirements.txt View File

@@ -0,0 +1 @@
torch

+ 45
- 0
tools/pnnx/python/setup.py.i View File

@@ -0,0 +1,45 @@
import sys
from setuptools import setup, find_packages

try:
from wheel.bdist_wheel import bdist_wheel as _bdist_wheel

class bdist_wheel(_bdist_wheel):
def finalize_options(self):
_bdist_wheel.finalize_options(self)
self.root_is_pure = False

except ImportError:
bdist_wheel = None

if sys.version_info < (3, 0):
sys.exit("Sorry, Python < 3.0 is not supported")

requirements = ["torch"]

setup(
name="pnnx",
version="${PACKAGE_VERSION}",
author="nihui",
author_email="nihuini@tencent.com",
description="pnnx is an open standard for PyTorch model interoperability.",
url="https://github.com/Tencent/ncnn/tree/master/tools/pnnx",
classifiers=[
"Programming Language :: C++",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
],
license="BSD-3",
python_requires=">=3.6",
packages=find_packages(),
package_dir={"": "."},
package_data={"pnnx": ["pnnx${PYTHON_MODULE_PREFIX}${PYTHON_MODULE_EXTENSION}"]},
install_requires=requirements,
cmdclass={"bdist_wheel": bdist_wheel},
)

+ 69
- 0
tools/pnnx/python/tests/test_convert.py View File

@@ -0,0 +1,69 @@
# Tencent is pleased to support the open source community by making ncnn available.
#
# Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
#
# Licensed under the BSD 3-Clause License (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
#
# https://opensource.org/licenses/BSD-3-Clause
#
# Unless required by applicable law or agreed to in writing, software distributed
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.

import pytest
import pnnx

import torch
import torch.nn as nn
import torch.nn.functional as F
from packaging import version

class Model(nn.Module):
def __init__(self):
super(Model, self).__init__()

def forward(self, x, y, z, w):
x = F.relu(x)
y = F.relu(y)
z = F.relu(z)
w = F.relu(w)
return x, y, z, w

def test_convert():
net = Model()
net.eval()

torch.manual_seed(0)
x = torch.rand(1, 16)
y = torch.rand(12, 2, 16)
z = torch.rand(1, 3, 12, 16)
w = torch.rand(1, 5, 7, 9, 11)

a0, a1, a2, a3 = net(x, y, z, w)

# export torchscript
mod = torch.jit.trace(net, (x, y, z, w))
mod.save("test_F_relu_convert.pt")

pnnx.convert("test_F_relu_convert.pt",[[1,16],[12,2,16],[1,3,12,16],[1,5,7,9,11]] , ["f32", "f32", "f32", "f32"],)

# fix aten::
import re
f=open('test_F_relu_convert_pnnx.py','r')
alllines=f.readlines()
f.close()
f=open('test_F_relu_convert_pnnx.py','w+')
for eachline in alllines:
a=re.sub('aten::','F.',eachline)
a=re.sub(r'\\', r'\\\\',a)
f.writelines(a)
f.close()
import sys
import os
sys.path.append(os.path.join(os.getcwd()))
import test_F_relu_convert_pnnx
b0, b1, b2, b3 = test_F_relu_convert_pnnx.test_inference()

assert torch.equal(a0, b0) and torch.equal(a1, b1) and torch.equal(a2, b2) and torch.equal(a3, b3)

+ 43
- 0
tools/pnnx/python/tests/test_dynamicinput_convert.py View File

@@ -0,0 +1,43 @@
# Tencent is pleased to support the open source community by making ncnn available.
#
# Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
#
# Licensed under the BSD 3-Clause License (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
#
# https://opensource.org/licenses/BSD-3-Clause
#
# Unless required by applicable law or agreed to in writing, software distributed
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.

import pytest
import pnnx

import torch
import torch.nn as nn
import torch.nn.functional as F
from packaging import version

class Model(nn.Module):
def __init__(self):
super(Model, self).__init__()

def forward(self, x):
x = F.relu(x)
return x

def test_export():
net = Model()
net.eval()

torch.manual_seed(0)
x = torch.rand(1, 16)

a0 = net(x)

mod = torch.jit.trace(net, x)
mod.save("test_F_relu_dconvert.pt")

pnnx.convert("test_F_relu_dconvert.pt", [1, 16], "f32", input_shapes2 = [1, 8], input_types2 = "f32")

+ 40
- 0
tools/pnnx/python/tests/test_dynamicinput_export.py View File

@@ -0,0 +1,40 @@
# Tencent is pleased to support the open source community by making ncnn available.
#
# Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
#
# Licensed under the BSD 3-Clause License (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
#
# https://opensource.org/licenses/BSD-3-Clause
#
# Unless required by applicable law or agreed to in writing, software distributed
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.

import pytest
import pnnx

import torch
import torch.nn as nn
import torch.nn.functional as F
from packaging import version

class Model(nn.Module):
def __init__(self):
super(Model, self).__init__()

def forward(self, x):
x = F.relu(x)
return x

def test_export():
net = Model()
net.eval()

torch.manual_seed(0)
x = torch.rand(1, 16)

a0 = net(x)

pnnx.export(net, "test_F_relu_dexport", x, input_shapes2 = [1, 8], input_types2 = "f32")

+ 67
- 0
tools/pnnx/python/tests/test_export.py View File

@@ -0,0 +1,67 @@
# Tencent is pleased to support the open source community by making ncnn available.
#
# Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
#
# Licensed under the BSD 3-Clause License (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
#
# https://opensource.org/licenses/BSD-3-Clause
#
# Unless required by applicable law or agreed to in writing, software distributed
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.

import pytest
import pnnx

import torch
import torch.nn as nn
import torch.nn.functional as F
from packaging import version

class Model(nn.Module):
def __init__(self):
super(Model, self).__init__()

def forward(self, x, y, z, w):
x = F.relu(x)
y = F.relu(y)
z = F.relu(z)
w = F.relu(w)
return x, y, z, w

def test_export():
net = Model()
net.eval()

torch.manual_seed(0)
x = torch.rand(1, 16)
y = torch.rand(12, 2, 16)
z = torch.rand(1, 3, 12, 16)
w = torch.rand(1, 5, 7, 9, 11)

a0, a1, a2, a3 = net(x, y, z, w)

pnnx.export(net, "test_F_relu_export", (x, y, z, w))

# import sys
# import os
# sys.path.append(os.path.join(os.getcwd()))

# fix aten::
import re
f=open('test_F_relu_export_pnnx.py','r')
alllines=f.readlines()
f.close()
f=open('test_F_relu_export_pnnx.py','w+')
for eachline in alllines:
a=re.sub('aten::','F.',eachline)
a=re.sub(r'\\', r'\\\\',a)
f.writelines(a)
f.close()

import test_F_relu_export_pnnx
b0, b1, b2, b3 = test_F_relu_export_pnnx.test_inference()

assert torch.equal(a0, b0) and torch.equal(a1, b1) and torch.equal(a2, b2) and torch.equal(a3, b3)

+ 63
- 0
tools/pnnx/python/tests/test_naiveinput_convert.py View File

@@ -0,0 +1,63 @@
# Tencent is pleased to support the open source community by making ncnn available.
#
# Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
#
# Licensed under the BSD 3-Clause License (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
#
# https://opensource.org/licenses/BSD-3-Clause
#
# Unless required by applicable law or agreed to in writing, software distributed
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.

import pytest
import pnnx

import torch
import torch.nn as nn
import torch.nn.functional as F
from packaging import version

class Model(nn.Module):
def __init__(self):
super(Model, self).__init__()

def forward(self, x):
x = F.relu(x)
return x

def test_export():
net = Model()
net.eval()

torch.manual_seed(0)
x = torch.rand(1, 16)

a0 = net(x)

mod = torch.jit.trace(net, x)
mod.save("test_F_relu_nconvert.pt")

pnnx.convert("test_F_relu_nconvert.pt", [1, 16], "f32")

import sys
import os
sys.path.append(os.path.join(os.getcwd()))
# fix aten::
import re
f=open('test_F_relu_nconvert_pnnx.py','r')
alllines=f.readlines()
f.close()
f=open('test_F_relu_nconvert_pnnx.py','w+')
for eachline in alllines:
a=re.sub('aten::','F.',eachline)
a=re.sub(r'\\', r'\\\\',a)
f.writelines(a)
f.close()

import test_F_relu_nconvert_pnnx
b0 = test_F_relu_nconvert_pnnx.test_inference()

assert torch.equal(a0, b0)

+ 60
- 0
tools/pnnx/python/tests/test_naiveinput_export.py View File

@@ -0,0 +1,60 @@
# Tencent is pleased to support the open source community by making ncnn available.
#
# Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
#
# Licensed under the BSD 3-Clause License (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
#
# https://opensource.org/licenses/BSD-3-Clause
#
# Unless required by applicable law or agreed to in writing, software distributed
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.

import pytest
import pnnx

import torch
import torch.nn as nn
import torch.nn.functional as F
from packaging import version

class Model(nn.Module):
def __init__(self):
super(Model, self).__init__()

def forward(self, x):
x = F.relu(x)
return x

def test_export():
net = Model()
net.eval()

torch.manual_seed(0)
x = torch.rand(1, 16)

a0 = net(x)

pnnx.export(net, "test_F_relu_nexport", x)

import sys
import os
sys.path.append(os.path.join(os.getcwd()))
# fix aten::
import re
f=open('test_F_relu_nexport_pnnx.py','r')
alllines=f.readlines()
f.close()
f=open('test_F_relu_nexport_pnnx.py','w+')
for eachline in alllines:
a=re.sub('aten::','F.',eachline)
a=re.sub(r'\\', r'\\\\',a)
f.writelines(a)
f.close()

import test_F_relu_nexport_pnnx
b0 = test_F_relu_nexport_pnnx.test_inference()

assert torch.equal(a0, b0)

+ 165
- 0
tools/pnnx/setup.py View File

@@ -0,0 +1,165 @@
import io
import os
import sys
import time
import re
import subprocess

from setuptools import setup, find_packages, Extension
from setuptools.command.build_ext import build_ext
from setuptools.command.install import install

def set_version():
pnnx_version = time.strftime("%Y%m%d", time.localtime())
return pnnx_version

# Parse environment variables
TORCH_INSTALL_DIR = os.environ.get("TORCH_INSTALL_DIR", "")
TORCHVISION_INSTALL_DIR = os.environ.get("TORCHVISION_INSTALL_DIR", "")
PROTOBUF_INCLUDE_DIR = os.environ.get("PROTOBUF_INCLUDE_DIR", "")
PROTOBUF_LIBRARIES = os.environ.get("PROTOBUF_LIBRARIES", "")
PROTOBUF_PROTOC_EXECUTABLE = os.environ.get("PROTOBUF_PROTOC_EXECUTABLE", "")
CMAKE_BUILD_TYPE = os.environ.get("CMAKE_BUILD_TYPE", "")
PNNX_BUILD_WITH_STATIC_CRT = os.environ.get("PNNX_BUILD_WITH_STATIC_CRT", "")
PNNX_WHEEL_WITHOUT_BUILD = os.environ.get("PNNX_WHEEL_WITHOUT_BUILD", "")


# Convert distutils Windows platform specifiers to CMake -A arguments
PLAT_TO_CMAKE = {
"win32": "Win32",
"win-amd64": "x64",
"win-arm32": "ARM",
"win-arm64": "ARM64",
}

# A CMakeExtension needs a sourcedir instead of a file list.
# The name must be the _single_ output extension from the CMake build.
# If you need multiple extensions, see scikit-build.
class CMakeExtension(Extension):
def __init__(self, name, sourcedir=""):
Extension.__init__(self, name, sources=[])
self.sourcedir = os.path.abspath(sourcedir)

class CMakeBuild(build_ext):
def build_extension(self, ext):
extdir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name)))
extdir = os.path.join(extdir, "pnnx")

# required for auto-detection of auxiliary "native" libs
if not extdir.endswith(os.path.sep):
extdir += os.path.sep

cfg = "Debug" if self.debug else "Release"

# CMake lets you override the generator - we need to check this.
# Can be set with Conda-Build, for example.
cmake_generator = os.environ.get("CMAKE_GENERATOR", "")

# Set Python_EXECUTABLE instead if you use PYBIND11_FINDPYTHON
# EXAMPLE_VERSION_INFO shows you how to pass a value into the C++ code
# from Python.
cmake_args = [
"-DCMAKE_RUNTIME_OUTPUT_DIRECTORY={}".format(extdir),
"-DCMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE={}".format(extdir),
"-DPython3_EXECUTABLE={}".format(sys.executable),
"-DCMAKE_BUILD_TYPE={}".format(cfg), # not used on MSVC, but no harm
]

if TORCH_INSTALL_DIR != "":
cmake_args.append("-DTorch_INSTALL_DIR=" + TORCH_INSTALL_DIR)
if TORCHVISION_INSTALL_DIR != "":
cmake_args.append("-DTorchVision_INSTALL_DIR=" + TORCHVISION_INSTALL_DIR)
if PROTOBUF_INCLUDE_DIR != "":
cmake_args.append("-DProtobuf_INCLUDE_DIR=" + PROTOBUF_INCLUDE_DIR)
if PROTOBUF_LIBRARIES != "":
cmake_args.append("-DProtobuf_LIBRARIES=" + PROTOBUF_LIBRARIES)
if PROTOBUF_PROTOC_EXECUTABLE != "":
cmake_args.append("-DProtobuf_PROTOC_EXECUTABLE=" + PROTOBUF_PROTOC_EXECUTABLE)
if CMAKE_BUILD_TYPE != "":
cmake_args.append("-DCMAKE_BUILD_TYPE=" + CMAKE_BUILD_TYPE)
if PNNX_BUILD_WITH_STATIC_CRT != "":
cmake_args.append("-DPNNX_BUILD_WITH_STATIC_CRT=" + PNNX_BUILD_WITH_STATIC_CRT)
build_args = []

if self.compiler.compiler_type == "msvc":
# Single config generators are handled "normally"
single_config = any(x in cmake_generator for x in {"NMake", "Ninja"})

# CMake allows an arch-in-generator style for backward compatibility
contains_arch = any(x in cmake_generator for x in {"ARM", "Win64"})

# Specify the arch if using MSVC generator, but only if it doesn't
# contain a backward-compatibility arch spec already in the
# generator name.
if not single_config and not contains_arch:
cmake_args += ["-A", PLAT_TO_CMAKE[self.plat_name]]

# Multi-config generators have a different way to specify configs
if not single_config:
cmake_args += [
"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{}={}".format(cfg.upper(), extdir)
]
build_args += ["--config", cfg]

# Set CMAKE_BUILD_PARALLEL_LEVEL to control the parallel build level
# across all generators.
if "CMAKE_BUILD_PARALLEL_LEVEL" not in os.environ:
# self.parallel is a Python 3 only way to set parallel jobs by hand
# using -j in the build_ext call, not supported by pip or PyPA-build.
if hasattr(self, "parallel") and self.parallel:
# CMake 3.12+ only.
build_args += ["-j{}".format(self.parallel)]
else:
build_args += ["-j2"]

if not os.path.exists(self.build_temp):
os.makedirs(self.build_temp)

if not (PNNX_WHEEL_WITHOUT_BUILD == "ON"):
subprocess.check_call(
["cmake", ext.sourcedir] + cmake_args, cwd=self.build_temp
)
subprocess.check_call(
["cmake", "--build", "."] + build_args, cwd=self.build_temp
)
else:
pass

if sys.version_info < (3, 0):
sys.exit("Sorry, Python < 3.0 is not supported")

requirements = ["torch"]

with io.open("README.md", encoding="utf-8") as h:
long_description = h.read()

setup(
name="pnnx",
version=set_version(),
author="nihui",
author_email="nihuini@tencent.com",
description="pnnx is an open standard for PyTorch model interoperability.",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/Tencent/ncnn/tree/master/tools/pnnx",
classifiers=[
"Programming Language :: C++",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
],
license="BSD-3",
python_requires=">=3.7",
packages=find_packages("python"),
package_data={"pnnx": ["pnnx", "pnnx.exe"]},
package_dir={"": "python"},
install_requires=requirements,
ext_modules=[CMakeExtension("pnnx")],
cmdclass={"build_ext": CMakeBuild},
)

Loading…
Cancel
Save