Skip to content

Commit

Permalink
feat(py): add scripts for building and uploading to PyPI (tensorflow#…
Browse files Browse the repository at this point in the history
…2236)

Add mechanism and scripts for building and uploading the Python distribution
package `tflite_micro` to PyPI. These scripts are intended mainly for use by CI
when generating packages for distribution via for PyPI, and won't be used by
most developers. Building a package for local use is still done via a normal
Bazel build.

Heavily comment the scripts with rationale and technical details of the
implementation.

Make significant updates to python/tflite_micro/README.md which explain
building, installing, and uploading the package to PyPI. Leave some cleanup of
the existing text for later.

Add a build setting `--//python/tflite_micro:compatibility_tag` for setting
:whl's platform compatibility tag. Unfortunately, it cannot derived
automatically from the execution environment by the current implementation of
@rules_python.

BUG=part of tensorflow#1484
  • Loading branch information
rkuester authored Sep 20, 2023
1 parent 9145957 commit e0e052d
Show file tree
Hide file tree
Showing 5 changed files with 354 additions and 19 deletions.
54 changes: 50 additions & 4 deletions python/tflite_micro/BUILD
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
load("@bazel_skylib//rules:common_settings.bzl", "string_flag")
load("@rules_python//python:defs.bzl", "py_library", "py_test")
load("@pybind11_bazel//:build_defs.bzl", "pybind_extension")
load("//python:py_namespace.bzl", "py_namespace")
Expand Down Expand Up @@ -166,17 +167,62 @@ expand_stamp_vars(
template = "README.pypi.md.in",
)

# Building the :whl or its descendants requires the following build setting to
# supply the Python compatibility tags for the wheel metadata.
string_flag(
name = "compatibility_tag",
build_setting_default = "local",
values = [
"cp310_cp310_manylinux_2_28_x86_64",
"cp311_cp311_manylinux_2_28_x86_64",
"local",
],
)

config_setting(
name = "cp310_cp310_manylinux_2_28_x86_64",
flag_values = {
":compatibility_tag": "cp310_cp310_manylinux_2_28_x86_64",
},
)

config_setting(
name = "cp311_cp311_manylinux_2_28_x86_64",
flag_values = {
":compatibility_tag": "cp311_cp311_manylinux_2_28_x86_64",
},
)

config_setting(
name = "local",
flag_values = {
":compatibility_tag": "local",
},
)

py_wheel(
name = "whl",
# This macro yields additional targets:
#
# - whl.publish: publish the package to PyPI via a command like:
#
# TWINE_USERNAME=__token__ TWINE_PASSWORD=pypi-*** \
# bazel run //python/tflite_micro:whl.publish -- --repository testpypi
# - whl.dist: build a properly named file under whl_dist/
#
abi = select({
":cp310_cp310_manylinux_2_28_x86_64": "cp310",
":cp311_cp311_manylinux_2_28_x86_64": "cp311",
":local": "none",
}),
description_file = ":description_file",
distribution = "tflite_micro",
platform = select({
":cp310_cp310_manylinux_2_28_x86_64": "manylinux_2_28_x86_64",
":cp311_cp311_manylinux_2_28_x86_64": "manylinux_2_28_x86_64",
":local": "any",
}),
python_tag = select({
":cp310_cp310_manylinux_2_28_x86_64": "cp310",
":cp311_cp311_manylinux_2_28_x86_64": "cp311",
":local": "py3",
}),
requires = [
"flatbuffers",
"numpy",
Expand Down
132 changes: 117 additions & 15 deletions python/tflite_micro/README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,127 @@
# TFLM Python Interpreter
# The `tflite_micro` Python Package

The TFLM interpreter can be invoked from Python by using the Python interpreter
wrapper in this directory.
This directory contains the `tflite_micro` Python package. The following is
mainly documentation for its developers.

## Usage
The `tflite_micro` package contains a complete TFLM interpreter built as a
CPython extension module. The build of simple Python packages may be driven by
standard Python package builders such as `build`, `setuptools`, and `flit`;
however, as TFLM is first and foremost a large C/C++ project, `tflite_micro`'s
build is instead driven by its C/C++ build system Bazel.

There are two ways to import the Python wrapper, either by using Bazel/Blaze, or
in near future by installing a PyPi package.
## Building and installing locally

### Bazel
### Building

#### Build
The Bazel target `//python/tflite_micro:whl.dist` builds a `tflite_micro`
Python *.whl* under the output directory `bazel-bin/python/tflite_micro/whl_dist`. For example:
```
% bazel build //python/tflite_micro:whl.dist
....
Target //python/tflite_micro:whl.dist up-to-date:
bazel-bin/python/tflite_micro/whl_dist
% tree bazel-bin/python/tflite_micro/whl_dist
bazel-bin/python/tflite_micro/whl_dist
└── tflite_micro-0.dev20230920161638-py3-none-any.whl
```

### Installing

Install the resulting *.whl* via pip. For example, in a Python virtual
environment:
```
% python3 -m venv ~/tmp/venv
% source ~/tmp/venv/bin/activate
(venv) $ pip install bazel-bin/python/tflite_micro/whl_dist/tflite_micro-0.dev20230920161638-py3-none-any.whl
Processing ./bazel-bin/python/tflite_micro/whl_dist/tflite_micro-0.dev20230920161638-py3-none-any.whl
....
Installing collected packages: [....]
```

The package should now be importable and usable. For example:
```
(venv) $ python
Python 3.10.12 (main, Jun 11 2023, 05:26:28) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import tflite_micro
>>> tflite_micro.postinstall_check.passed()
True
>>> i = tflite_micro.runtime.Interpreter.from_file("foo.tflite")
>>> # etc.
```

## Building and uploading to PyPI

The *.whl* generated above is unsuitable for distribution to the wider world
via PyPI. The extension module is inevitably compiled against a particular
Python implementation and platform C library. The resulting package is only
binary-compatible with a system running the same Python implementation and a
compatible (typically the same or newer) C library.

The solution is to distribute multiple *.whl*s, one built for each Python
implementation and platform combination. TFLM accomplishes this by running
Bazel builds from within multiple, uniquely configured Docker containers. The
images used are based on standards-conforming images published by the Python
Package Authority (PyPA) for exactly such use.

Python *.whl*s contain metadata used by installers such as `pip` to determine
which distributions (*.whl*s) are compatible with the target platform. See the PyPA
specification for [platform compatibility
tags](https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/).

### Building

In an environment with a working Docker installation, run the script
`python/tflite_micro/pypi_build.sh <python-tag>` once for each tag. The
script's online help (`--help`) lists the available tags. The script builds an
appropriate Docker container and invokes a Bazel build and test within it.
For example:
```
% python/tflite_micro/pypi_build.sh cp310
[+] Building 2.6s (7/7) FINISHED
=> writing image sha256:900704dad7fa27938dcc1c5057c0e760fb4ab0dff676415182455ae66546bbd4
bazel build //python/tflite_micro:whl.dist \
--//python/tflite_micro:compatibility_tag=cp310_cp310_manylinux_2_28_x86_64
bazel test //python/tflite_micro:whl_test \
--//python/tflite_micro:compatibility_tag=cp310_cp310_manylinux_2_28_x86_64
//python/tflite_micro:whl_test
Executed 1 out of 1 test: 1 test passes.
Output:
bazel-pypi-out/tflite_micro-0.dev20230920031310-cp310-cp310-manylinux_2_28_x86_64.whl
```

By default, *.whl*s are generated under the output directory `bazel-pypi-out/`.

### Uploading to PyPI

Upload the generated *.whl*s to PyPI with the script
`python/tflite_micro/pypi_upload.sh`. This script lightly wraps the standard
upload tool `twine`. A PyPI authentication token must be assigned to
`TWINE_PASSWORD` in the environment. For example:
```
% export TWINE_PASSWORD=pypi-AgENdGV[....]
% ./python/tflite_micro/pypi_upload.sh --test-pypi bazel-pypi-out/tflite_micro-*.whl
Uploading distributions to https://test.pypi.org/legacy/
Uploading tflite_micro-0.dev20230920031310-cp310-cp310-manylinux_2_28_x86_64.whl
Uploading tflite_micro-0.dev20230920031310-cp311-cp311-manylinux_2_28_x86_64.whl
View at:
https://test.pypi.org/project/tflite-micro/0.dev20230920031310/
```

See the script's online help (`--help`) for more.

## Using `tflite_micro` from within the TFLM source tree

:construction:
*The remainder of this document is under construction and may contain some
obsolete information.*
:construction:

The only package that needs to be included in the `BUILD` file is
`//python/tflite_micro:runtime`. It contains all
the correct dependencies to build the Python interpreter.

### PyPi

Work in progress.

### Examples

Depending on the workflow, the package import path may be slightly different.
Expand Down Expand Up @@ -55,7 +157,7 @@ print(tflm_interpreter.get_input_details(0))
print(tflm_interpreter.get_output_details(0))
```

## Technical Details
### Technical Details

The Python interpreter uses [pybind11](https://github.com/pybind/pybind11) to
expose an evolving set of C++ APIs. The Bazel build leverages the
Expand All @@ -64,7 +166,7 @@ expose an evolving set of C++ APIs. The Bazel build leverages the
The most updated Python APIs can be found in
`python/tflite_micro/runtime.py`.

## Custom Ops
### Custom Ops

The Python interpreter works with models with
[custom ops](https://www.tensorflow.org/lite/guide/ops_custom) but special steps
Expand Down Expand Up @@ -126,7 +228,7 @@ The interpreter will then perform a dynamic lookup for the symbol called
properly included in TFLM's op resolver. This approach is very similar to
TFLite's custom op support.

## Print Allocations
### Print Allocations

The Python interpreter can also be used to print memory arena allocations. This
is very helpful to figure out actual memory arena usage.
Expand Down
16 changes: 16 additions & 0 deletions python/tflite_micro/pypi_build.dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Use the Python Packaging Authority's reference build environment
# for binary extensions. Binary extensions are typically built and distributed
# for each target Python version and OS platform. The reference build
# environment contains Python installations for each version, and a C/C++
# toolchain specified for maximum compatibility among x86_64 Linux paltforms.
FROM quay.io/pypa/manylinux_2_28_x86_64

# Install bazel (via bazelisk)
ENV BAZELISK=https://github.com/bazelbuild/bazelisk/releases/download/v1.18.0/bazelisk-linux-amd64
ENV BAZEL=/usr/local/bin/bazel
RUN curl --output $BAZEL --location $BAZELISK && chmod 755 $BAZEL

# Append the location of the C/C++ toolchain to the default PATH, where
# bazel expects to find it. The reference environment provides the location
# (typically somewhere under /opt) in DEVTOOLSET_ROOTPATH.
RUN echo "PATH="${PATH}:/${DEVTOOLSET_ROOTPATH}"" >>/etc/environment
114 changes: 114 additions & 0 deletions python/tflite_micro/pypi_build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/bin/sh

# Copyright 2023 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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.

set -e

OUT_DIR_DEFAULT=bazel-pypi-out

USAGE="$(basename $0) <python-tag> [<output-directory>]
Build a Python wheel for public release to PyPI using a special Docker build
container. Uses bazel, but does not pollute the WORKSPACE's default cache.
<python-tag> must be one of the supported interpreters:
cp310
cp311
<output-directory> defaults to $OUT_DIR_DEFAULT.
"

case "$1" in
cp310|cp311)
PY_TAG=$1
OUTDIR=$(realpath ${2:-$OUT_DIR_DEFAULT})
mkdir -p $OUTDIR
break
;;
*)
echo usage: "$USAGE" >&2
exit 1
esac

SRCDIR=$(realpath .)
if ! test -f $SRCDIR/WORKSPACE; then
echo "error: must run from the top of the source tree" >&2
exit 1
fi

# Remove Bazel's workspace symlinks so they'll be rewritten below, pointing into
# OUTDIR.
find . -maxdepth 1 -type l -name bazel-\* | xargs rm -f

# Build the Docker image from its source file. Don't pollute the public list of
# images by tagging; just use the image's ID.
DOCKERFILE=python/tflite_micro/pypi_build.dockerfile
IMAGE_ID_FILE=$OUTDIR/image-id
docker build - --iidfile $IMAGE_ID_FILE <$DOCKERFILE
IMAGE_ID=$(cat $IMAGE_ID_FILE)

# Build the Python package within an ephemeral container.
docker run \
--rm \
--interactive \
--mount type=bind,source=$SRCDIR,destination=$SRCDIR \
--mount type=bind,source=$OUTDIR,destination=$OUTDIR \
--workdir $SRCDIR \
--env USER=$(id -un) \
$IMAGE_ID \
/bin/bash -s -e -x -u \
<<EOF
# Setup the Python compatibility tags. The PY_ABI always matches the Python
# interpreter tag. The platform tag is supplied by the build image in the
# environment variable AUDITWHEEL_PLAT.
PY_ABI=$PY_TAG
PY_PLATFORM=\$AUDITWHEEL_PLAT
PY_COMPATIBILITY=${PY_TAG}_\${PY_ABI}_\${PY_PLATFORM}
# Link the desired Python version in the PATH where bazel will find it. The
# build image contains many differnet Python installations as options.
ln -sf /opt/python/$PY_TAG-$PY_TAG/bin/* /usr/bin
# Bazelisk fails if it can't check HOME for a .rc file.
export HOME=$OUTDIR
# Bazelisk, bazel, and pip all need a writable cache directory.
export XDG_CACHE_HOME=$OUTDIR/cache
# Build the wheel via bazel, using the Python compatibility tag matching the
# build environment. Drop root privledges and run as the invoking user.
# Relocate the bazel cache to keep the cache used for each toolchain
# separate.
setpriv --reuid=$(id -u) --regid=$(id -g) --clear-groups \
bazel --output_user_root=$OUTDIR/$PY_TAG-out \
build \
//python/tflite_micro:whl.dist \
--//python/tflite_micro:compatibility_tag=\$PY_COMPATIBILITY
# Test, in the container environment
setpriv --reuid=$(id -u) --regid=$(id -g) --clear-groups \
bazel --output_user_root=$OUTDIR/$PY_TAG-out \
test \
//python/tflite_micro:whl_test \
--//python/tflite_micro:compatibility_tag=\$PY_COMPATIBILITY
EOF

# Make the output directory tree writable so it can be removed easily by the
# user with `rm -rf $OUTDIR`. Bazel leaves it write-protected.
chmod -R +w $OUTDIR

# Copy the generated wheel file to the root of the $OUTDIR.
cp bazel-bin/python/tflite_micro/whl_dist/*.whl $OUTDIR
echo "Output:\n$(ls $OUTDIR/*.whl)"
Loading

0 comments on commit e0e052d

Please sign in to comment.