本章的主題是TF中的混合編程,以Python與C/C++混合編程為例.
按進度來說,現(xiàn)在應(yīng)該寫點TF使用教程,讓大家熟悉一下tensorflow的使用,但是我發(fā)現(xiàn)現(xiàn)在這方面的資料和書籍已經(jīng)很多了,這里就不再贅述了,畢竟時間有限,留給更有意義的事情。
做到熟悉TF使用的最好的方式就是動手實踐具體的例子,官網(wǎng)提供的教程就不錯。我建議繼續(xù)閱讀本章之前,讀者通過這些實際操作的例子,熟悉一下TensorFlow的使用流程。
下面進入本章的主題。
連接兩個世界的傳送門
回憶上一章中,我們編譯、安裝tensorflow的方式如下:
首先,bazel build目標(biāo)
$ bazel build --config=opt //tensorflow/tools/pip_package:build_pip_package
然后,啟動目標(biāo)程序,生成wheel格式安裝包到tmp/tensorflow_pkg目錄:
$ bazel-bin/tensorflow/tools/pip_package/build_pip_package /tmp/tensorflow_pkg
最后,通過tensorflow的pip包管理器安裝tensorflow的安裝包:
$ sudo pip install /tmp/tensorflow_pkg/tensorflow-1.6.0-py2-none-any.whl
這個流程我相信大家都已經(jīng)很熟悉了,我們就從這個頂級的構(gòu)建目標(biāo)入手,自頂向下分析TF中Python的調(diào)用是如何進入C/C++世界的。
第二章中我們提到,上面命令中的構(gòu)建目標(biāo) //tensorflow/tools/pip_package:build_pip_package 其實是一個可執(zhí)行的 shell 腳本,通過 bazel 的 sh_binary 規(guī)則生成,sh_binary 的規(guī)則會將腳本依賴的文件拷貝或則生成(如果被依賴項也是bazel的規(guī)則定義的目標(biāo)的話)到runfiles目錄下,腳本build_pip_package.sh的工作就是將這些文件打包成一個wheel格式的安裝包。
第一章中,我們學(xué)習(xí)到tf的內(nèi)核是由C++寫成的,支持的前端API有Python,Go,Java,這些前端語言接口基本都是對C API的封裝;那么我們把TF的整個工程分為兩個部門來分別學(xué)習(xí):接口部分和內(nèi)核部分;我們還知道,接口部分通往內(nèi)核部分的最終都會通過//tensorflow/c:c_api。
因此,我們來看下Python的頂級目標(biāo)與c_api的關(guān)系。這里需要用到了 bazel 的query命令。
我們在tf工程根目錄執(zhí)行下面的命令:
$ bazel query 'allpaths(//tensorflow/tools/pip_package:build_pip_package, //tensorflow/c:c_api)' --output graph | dot -v -Tpng -o dep_paths.in
命令的作用是找到bazel目標(biāo) //tensorflow/tools/pip_package:build_pip_package 到目標(biāo) //tensorflow/c:c_api 的所有依賴路徑,并將結(jié)果輸出為圖片:

這用到了graphviz的dot命令,graphviz的安裝也很簡單:
$ sudo apt-get install graphviz
圖3是一個有向圖,每一條有向邊代表源節(jié)點對目的節(jié)點的依賴關(guān)系,可以看到涉及的工程目標(biāo)非常多,依賴也繁雜,但是還是可以分析的;首先,我們注意這兩個節(jié)點:其中一個節(jié)點只有出度沒有入度,這就是我們都頂級構(gòu)建目標(biāo) //tensorflow/tools/pip_package:build_pip_package

<center>圖4:build_pip_package 節(jié)點</center>
另一個節(jié)點只有入度,沒有出度,就是我們的 //tensorflow/c:c_api 節(jié)點:

<center>圖5:c_api節(jié)點</center>
另外,還有一個節(jié)點比較引人注意,那就是 //tensorflow/python:pywrap_tensorflow_internal,我們注意到這樣一個特征,整張圖在這個節(jié)點上分成了上下兩個"團體",每個團體內(nèi)部的依賴比較復(fù)雜,暫時先不用管,但是上層"團體"對下層"團體"的依賴都經(jīng)過//tensorflow/python:pywrap_tensorflow_internal節(jié)點:

<center>圖6:pywrap_tensorflow_internal</center>
直觀感覺,這個節(jié)點至關(guān)重要,是連接了兩個世界的"傳送門"。
Python擴展pywrap_tensorflow_internal
找到定義pywrap_tensorflow_internal的BUILD文件//tensorflow/python/BUILD:
tf_py_wrap_cc(
name = "pywrap_tensorflow_internal",
srcs = ["tensorflow.i"],
swig_includes = [
"client/device_lib.i",
"client/events_writer.i",
"client/tf_session.i",
"client/tf_sessionrun_wrapper.i",
"framework/cpp_shape_inference.i",
"framework/python_op_gen.i",
"grappler/cost_analyzer.i",
"grappler/tf_optimizer.i",
"lib/core/py_func.i",
"lib/core/strings.i",
"lib/io/file_io.i",
"lib/io/py_record_reader.i",
"lib/io/py_record_writer.i",
"platform/base.i",
"training/quantize_training.i",
"training/server_lib.i",
"util/kernel_registry.i",
"util/port.i",
"util/py_checkpoint_reader.i",
"util/stat_summarizer.i",
"util/transform_graph.i",
],
deps = [
":cost_analyzer_lib",
":cpp_shape_inference",
":kernel_registry",
":numpy_lib",
":py_func_lib",
":py_record_reader_lib",
":py_record_writer_lib",
":python_op_gen",
":tf_session_helper",
"http://tensorflow/c:c_api",
"http://tensorflow/c:checkpoint_reader",
"http://tensorflow/c:tf_status_helper",
"http://tensorflow/core/distributed_runtime/rpc:grpc_server_lib",
"http://tensorflow/core/distributed_runtime/rpc:grpc_session",
"http://tensorflow/core/grappler:grappler_item",
"http://tensorflow/core/grappler:grappler_item_builder",
"http://tensorflow/core/grappler/clusters:single_machine",
"http://tensorflow/core/grappler/optimizers:meta_optimizer",
"http://tensorflow/core:lib",
"http://tensorflow/core:reader_base",
"http://tensorflow/core/debug",
"http://tensorflow/core/distributed_runtime:server_lib",
"http://tensorflow/tools/graph_transforms:transform_graph_lib",
"http://tensorflow/tools/tfprof/internal:print_model_analysis",
"http://util/python:python_headers",
] + (tf_additional_lib_deps() +
tf_additional_plugin_deps() +
tf_additional_verbs_deps() +
tf_additional_mpi_deps()),
)
這里的tf_py_wrap_cc是bazel的宏函數(shù)。那么宏函數(shù)tf_py_wrap_cc的作用是什么?這里srcs和swig_includes屬性里的擴展名為 .i 文件又是什么呢?
找到宏函數(shù)tf_py_wrap_cc定義的文件 /tensorflow/tensorflow.bzl :
def tf_py_wrap_cc(name,
srcs,
swig_includes=[],
deps=[],
copts=[],
**kwargs):
module_name = name.split("/")[-1]
# Convert a rule name such as foo/bar/baz to foo/bar/_baz.so
# and use that as the name for the rule producing the .so file.
cc_library_name = "/".join(name.split("/")[:-1] + ["_" + module_name + ".so"])
cc_library_pyd_name = "/".join(
name.split("/")[:-1] + ["_" + module_name + ".pyd"])
extra_deps = []
_py_wrap_cc(
name=name + "_py_wrap",
srcs=srcs,
swig_includes=swig_includes,
deps=deps + extra_deps,
toolchain_deps=["http://tools/defaults:crosstool"],
module_name=module_name,
py_module_name=name)
...
...
函數(shù)的前半部分,主要就是調(diào)用一個自定義規(guī)則 _py_wrap_cc,此規(guī)則聲明同在tensorflow/tensorflow.bzl文件中,內(nèi)容如下:
_py_wrap_cc = rule(
#
# 定義規(guī)則的輸入屬性名稱,數(shù)據(jù)類型,是否必須以及默認(rèn)值等
#
attrs={
"srcs":
attr.label_list(
mandatory=True,
allow_files=True,),
"swig_includes":
attr.label_list(
cfg="data",
allow_files=True,),
"deps":
attr.label_list(
allow_files=True,
providers=["cc"],),
"toolchain_deps":
attr.label_list(
allow_files=True,),
"module_name":
attr.string(mandatory=True),
"py_module_name":
attr.string(mandatory=True),
"_swig":
attr.label(
default=Label("@swig//:swig"),
executable=True,
cfg="host",),
"_swiglib":
attr.label(
default=Label("@swig//:templates"),
allow_files=True,),
},
#
# 定義規(guī)則的輸出
#
outputs={
"cc_out": "%{module_name}.cc",
"py_out": "%{py_module_name}.py",
},
#
# 定義規(guī)則的實現(xiàn)函數(shù)
#
implementation=_py_wrap_cc_impl,)
規(guī)則聲明中,定義了規(guī)則的屬性、屬性的變量類型以及默認(rèn)取值、規(guī)則的輸出以及實現(xiàn)函數(shù);_py_wrap_cc規(guī)則的實現(xiàn)在函數(shù)_py_wrap_cc_impl中,我們將仔細分析一下這個函數(shù)。
注:宏函數(shù)和自定義規(guī)則是bazel支持的兩個擴展機制,兩則是有差別的,
限于篇幅這里就不仔細介紹了??梢詤⒖?[官網(wǎng)](https://docs.bazel.build/versions/master/skylark/concepts.html)。
暫時讀者只要能更隨本人思路就可以,細節(jié)可以之后再去學(xué)習(xí),本人盡力保證在讀者不熟悉bazel
的情況下也能理解本文內(nèi)容。
在詳細分析_py_wrap_cc_impl函數(shù)之前,我們需要補充一點關(guān)于Python和C/C++混合編程的知識。
SWIG

Python和C/C++的混合編程存在兩種編程模式:擴展與嵌入,這里主要介紹前一種。C/C++編寫Pyton擴展過程如下:



可以看到,手動完成擴展的編寫還是挺低效的,最后還需要將編寫完的封裝代碼和源碼一起編譯成動態(tài)鏈接庫,限于篇幅,這里就不具體介紹了;我們來介紹一個自動化完成擴展編寫的工具SWIG。

SWIG是一個接口編譯工具,連接C/C++代碼與腳本語言Perl.Python,Ruby,Tcl的橋梁。SWIG為C/C++頭文件自動生成包裝代碼,提供給腳本語言使用。
我們來看一個例子,假如我們有這樣一個C代碼文件example.c,其中包含了想要提供給其他語言如Perl,Python,java,C#代碼使用的方法:
/* File : example.c */
#include <time.h>
double My_variable = 3.0;
int fact(int n) {
if (n <= 1) return 1;
else return n*fact(n-1);
}
int my_mod(int x, int y) {
return (x%y);
}
char *get_time()
{
time_t ltime;
time(<ime);
return ctime(<ime);
}
那么首先我們需要創(chuàng)建一個"接口文件",擴展名為 .i:
/* example.i */
%module example
%{
/* Put header files here or function declarations like below */
extern double My_variable;
extern int fact(int n);
extern int my_mod(int x, int y);
extern char *get_time();
%}
extern double My_variable;
extern int fact(int n);
extern int my_mod(int x, int y);
extern char *get_time();
然后就可以構(gòu)建其他語言的模塊了,例如可以執(zhí)行如下命令構(gòu)建Python模塊:
$ swig -python example.i
$ gcc -c example.c example_wrap.c -I/usr/local/include/python2.1
$ ld -shared example.o example_wrap.o -o _example.so
然后就可以調(diào)用生成的Python模塊了:
>>> import example
>>> example.fact(5)
120
>>> example.my_mod(7,3)
1
>>> example.get_time()
'Sun Feb 11 23:01:07 1996'
>>>
可以通過執(zhí)行下列命令生成Java模塊:
$ swig -java example.i
$ gcc -c example.c example_wrap.c -I/c/jdk1.3.1/include -I/c/jdk1.3.1/include/win32
$ gcc -shared example.o example_wrap.o -mno-cygwin -Wl, --add-stdcall-alias -o example.dll
然后可以編寫java代碼,調(diào)用此模塊:
/* file main.java */
public class main{
public static void main(String argv[]){
System.loadLobrary('example');
System.out.println(example.getMy_variable());
System.out.println(example.fact(5));
System.out.println(example.get_time());
}
}
最后執(zhí)行調(diào)用:
$ javac main.java
$ java main
3.0
120
Mon Mar 4 18:20:31 2002
$
有了這些準(zhǔn)備知識后,我們可以開始分析_py_wrap_cc_impl函數(shù)了:
# Bazel rules for building swig files.
def _py_wrap_cc_impl(ctx):
##
## 下面的代碼在構(gòu)造SWIG的參數(shù)
##
srcs = ctx.files.srcs
if len(srcs) != 1:
fail("Exactly one SWIG source file label must be specified.", "srcs")
module_name = ctx.attr.module_name
src = ctx.files.srcs[0]
inputs = set([src])
inputs += ctx.files.swig_includes
for dep in ctx.attr.deps:
inputs += dep.cc.transitive_headers
inputs += ctx.files._swiglib
inputs += ctx.files.toolchain_deps
swig_include_dirs = set(_get_repository_roots(ctx, inputs))
swig_include_dirs += sorted([f.dirname for f in ctx.files._swiglib])
##
## swig的命令行參數(shù):-c++表示啟動C++解析,-python表示輸出python的
## wrapper代碼,-module設(shè)置模塊名稱,-o表示輸出文件名稱,-outdir
## 表示輸出目錄路徑,-l表示包含的SWIG的庫文件名稱,包括用戶提供的.i文件
## 以及需要的SWIG庫文件(也是一些.i文件),-I表示把參數(shù)路徑添
## 加到SWIG的include查找路徑。
##
args = [
"-c++", "-python", "-module", module_name, "-o", ctx.outputs.cc_out.path,
"-outdir", ctx.outputs.py_out.dirname
]
args += ["-l" + f.path for f in ctx.files.swig_includes]
args += ["-I" + i for i in swig_include_dirs]
args += [src.path]
outputs = [ctx.outputs.cc_out, ctx.outputs.py_out]
##
## 調(diào)用SWIG命令,生成swig files:ctx.action函數(shù)會啟動一個
## 可執(zhí)行文件或腳本executable, 啟動參數(shù)arguments, inputs
## 表示表示輸入文件,outputs表示輸出文件。
##
ctx.action(
executable=ctx.executable._swig,
arguments=args,
inputs=list(inputs),
outputs=outputs,
mnemonic="PythonSwig",
progress_message="SWIGing " + src.path)
return struct(files=set(outputs))
C/C++的Python插件封裝代碼通過SWIG生成之后,就可以編譯Python的插件了,這就是函數(shù)tf_py_wrap_cc后半部分所完成的工作:
def tf_py_wrap_cc(name,
srcs,
swig_includes=[],
deps=[],
copts=[],
**kwargs):
...
...
##
## module_name + ".cc"是上面介紹的_py_wrap_cc規(guī)則的輸出文件
## 也就是C/C++代碼的Python封裝代碼,與dep中的C/C++代碼一起,
## 通過cc_binary規(guī)則,生成動態(tài)鏈接庫cc_library_name。
##
native.cc_binary(
name=cc_library_name,
srcs=[module_name + ".cc"],
copts=(copts + [
"-Wno-self-assign", "-Wno-sign-compare", "-Wno-write-strings"
] + tf_extension_copts()),
linkopts=tf_extension_linkopts() + extra_linkopts,
linkstatic=1,
linkshared=1,
deps=deps + extra_deps)
##
## 定義一個生成規(guī)則,執(zhí)行拷貝命令cp,將動態(tài)鏈接庫cc_library_name拷貝一份,
## 名稱為cc_library_pyd_name(python的pyd文件實際也就是windows平臺下動態(tài)鏈接庫,
## 只不過擴展名不一樣而已)
##
native.genrule(
name="gen_" + cc_library_pyd_name,
srcs=[":" + cc_library_name],
outs=[cc_library_pyd_name],
cmd="cp $< $@",)
##
## py_library規(guī)則定義一個python庫目標(biāo),如果是windows平臺下
## 則依賴.pyd文件cc_library_pyd_name,這會出發(fā)上面的拷貝動作,其他
## 平臺下,則依賴.so文件cc_library_name
native.py_library(
name=name,
srcs=[":" + name + ".py"],
srcs_version="PY2AND3",
data=select({
clean_dep("http://tensorflow:windows"): [":" + cc_library_pyd_name],
"http://conditions:default": [":" + cc_library_name],
}))
最后,執(zhí)行bazel build命令,在windows下,會生成下列文件:
pywrap_tensorflow_internal.cc
pywrap_tensorflow_internal.py
_pywrap_tensorflow_internal.pyd
而在非window平臺下,則生成下列文件:
pywrap_tensorflow_internal.cc
pywrap_tensorflow_internal.py
_pywrap_tensorflow_internal.so
小結(jié)
總結(jié)一下,本章中,我們通過工具bazel query,找到了混合編程中鏈接兩個世界的模塊pywrap_tensorflow_internal,實際上它就是Python的一個擴展,python的代碼通過這個擴展就可以調(diào)用底層的C/C++代碼了。然后分析此工程的過程中,引入了SWIG工具,它使得C/C++代碼很方便的導(dǎo)出到各種其他的腳本語言。