CentOS 6でPython 3でTensorFlowを使う方法、もしくはdynamic linkerとshared objectの差し替え

Python 3

CentOS 6のopensslのバージョン(1.0.1e)の関係で、Python 3.7以上はビルドが失敗します。

今回は3.7にこだわりはないので、pyenvでPython 3.6の最新の3.6.8をインストールして、pipでtensorflowをインストールします。詳細は割愛ググってね。

glibc, stdc++

インストールに成功しても、使おうとするとエラーが出ます。

$ python3.6 -c 'import tensorflow; print("hello")'
Traceback (most recent call last):
...
ImportError: /lib64/libc.so.6: version `GLIBC_2.16' not found (required by /home/ore/.pyenv/versions/3.6.8/lib/python3.6/site-packages/tensorflow/python/_pywrap_tensorflow_internal.so)
...

$ ldd /home/ore/.pyenv/versions/3.6.8/lib/python3.6/site-packages/tensorflow/python/_pywrap_tensorflow_internal.so |& grep 'not found' | awk '{print $4}'
`GLIBC_2.16'
`GLIBC_2.14'
`GLIBC_2.17'
`GLIBC_2.15'
`GLIBCXX_3.4.15'
`GLIBCXX_3.4.19'
`GLIBCXX_3.4.14'
`CXXABI_1.3.7'
`GLIBCXX_3.4.17'
`GLIBCXX_3.4.18'
`CXXABI_1.3.5'
`GLIBC_2.14'
`GLIBC_2.16'
`GLIBC_2.17'
`GLIBCXX_3.4.14'
`GLIBCXX_3.4.18'
`CXXABI_1.3.5'
`GLIBCXX_3.4.15'
`GLIBCXX_3.4.19'
`GLIBCXX_3.4.17'
`CXXABI_1.3.7'

どようやらglibc 2.17とlibstdc++ 3.4.19が必要そうです。

さすがにシステムのglibcを置き換えるのは怖いので、別のディレクトリに展開して今回のpython3.6はそこのshared objectを使うようにします。

glibcとlibstdc++は、https://pkgs.org/ で探してCentOS 7のを拝借します。

$ cd ~/tmp/
$ wget \
  http://mirror.centos.org/centos/7/updates/x86_64/Packages/glibc-2.17-260.el7_6.5.x86_64.rpm \
  http://mirror.centos.org/centos/7/updates/x86_64/Packages/glibc-devel-2.17-260.el7_6.5.x86_64.rpm \
  http://mirror.centos.org/centos/7/os/x86_64/Packages/libstdc++-4.8.5-36.el7.x86_64.rpm \
  http://mirror.centos.org/centos/7/os/x86_64/Packages/libstdc++-devel-4.8.5-36.el7.x86_64.rpm \
  ;

~/lib/ の下に展開します。

$ cd ~/lib/
$ for p in ~/tmp/*.rpm; do echo $p; rpm2cpio $p | cpio -idv; done
$ ls -F
etc/  lib64/  sbin/  usr/  var/

以下、パス指定が長くなるので簡略化のため変数に入れときます。

py36="$HOME/.pyenv/versions/3.6.8/bin/python3.6"
pywrap_tensorflow_internal="$HOME/.pyenv/versions/3.6.8/lib/python3.6/site-packages/tensorflow/python/_pywrap_tensorflow_internal.so"

my_ld="$HOME/lib/lib64/ld-linux-x86-64.so.2"
my_libs="$HOME/lib/usr/lib64:$HOME/lib/lib64"

ld-linux.so はコマンドとしても実行できて、 --library-path でdynamic loadするshared objectのパスを指定できます。環境変数 LD_LIBRARY_PATH でも同様のことができますが、作用範囲が子プロセスまで及ぶかどうかという違いがあります。

ld-linux.so 経由で python3.6 を起動して、先程エラーになったコードを実行してみます。

$ $my_ld --library-path $my_libs $py36 -c 'import tensorflow; print("hello")'
hello

今度は正常終了しました!やったネ!!

child process

早速、最終的に走らせたいスクリプトを実行してみると…

$ $my_ld --library-path $my_libs $py36 oreno.py
* Serving Flask app "server" (lazy loading)
* Environment: production
...
Traceback (most recent call last):
...
ImportError: /lib64/libc.so.6: version `GLIBC_2.16' not found (required by /home/ore/.pyenv/versions/3.6.8/lib/python3.6/site-packages/tensorflow/python/_pywrap_tensorflow_internal.so)
...

失敗しました。

エラーメッセージからすると、システムのglibcをロードしてしまっているようです。strace -f -s 1000 $my_ld ... で確認してみると、後半で $my_ld を介さないで $py36execve して異常終了していました。詳細は確認してませんが、どっかで fork してるんじゃないかと思います。

子プロセスにも新しいglibcの場所を教えてあげればよいので、試しに LD_LIBRARY_PATH をセットして実行してみると…

$ env LD_LIBRARY_PATH=$my_libs $my_ld --library-path $my_libs $py36 oreno.py
...
/home/ore/.pyenv/versions/3.6.8/bin/python3.6: relocation error: /home/ore/lib/lib64/libc.so.6: symbol _dl_starting_up, version GLIBC_PRIVATE not defined in file ld-linux-x86-64.so.2 with link time reference

失敗しました。

たぶん、システムの /lib64/ld-linux-x86-64.so.2 で新しいglibcをロードしようとして失敗しているんだと思います。

もし、成功したとしても、LD_LIBRARY_PATH は作用範囲が大きいので、他の方法があれば避けたい手段です。

patchelf

Linuxの実行ファイル形式のELFには、dynamic linker (interpreter) とライブラリのサーチパス(runpath)を埋め込むことができます。これらは readelfpatchelf で確認することができます。

CentOS 6用の patchelf はEPELにあります。

$ readelf -a /usr/bin/mailq | grep -e interpreter -e runpath
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
 0x000000000000001d (RUNPATH)            Library runpath: [/usr/lib/postfix]

$ patchelf --print-interpreter --print-rpath /usr/bin/mailq
 /lib64/ld-linux-x86-64.so.2
 /usr/lib/postfix

patchelf はこれらの情報を変更することができるので、$py36interpreterとrunpathを変更してしまえば、 $my_ld --library-path $my_libs の前置が不要になるというわけです。

やってみましょう。

$ cp -p $py36 ${py36}.bak
$ patchelf --set-interpreter $my_ld --set-rpath $my_libs $py36

さっそく、$my_ld の前置なしで実行してみると…

$ $py36 -c 'import tensorflow; print("hello")'
Traceback (most recent call last):
...
ImportError: /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.15' not found (required by /home/ore/.pyenv/versions/3.6.8/lib/python3.6/site-packages/tensorflow/python/_pywrap_tensorflow_internal.so)

エラーが出ました。が、これは想定内です。

$py36 は期待したとおり新しいglibcをロードできたのですが、_pywrap_tensorflow_internal.so がlibstdc++をロードしようとして新しいのが見つけられずエラーになっているわけです。(glibcは既に $py36 が新しいのをロード済みなので大丈夫)

なので、_pywrap_tensorflow_internal.so もrunpathを書き換えてしまいましょう。

_pywrap_tensorflow_internal.so は既にrunpathが設定されているので、それに追加する感じでセットします。(純粋なshared objectなのでinterpreterの変更の必要はありません)

$ORIGIN が展開されないようにシングルクォートするのを忘れないでください。

$ patchelf --print-rpath $pywrap_tensorflow_internal
$ORIGIN/../../_solib_k8/_U_S_Stensorflow_Spython_C_Upywrap_Utensorflow_Uinternal.so___Utensorflow:$ORIGIN/:$ORIGIN/..

$ cp -p $pywrap_tensorflow_internal ${pywrap_tensorflow_internal}.bak
$ patchelf --set-rpath '$ORIGIN/../../_solib_k8/_U_S_Stensorflow_Spython_C_Upywrap_Utensorflow_Uinternal.so___Utensorflow:$ORIGIN/:$ORIGIN/..'":$my_libs" $pywrap_tensorflow_internal

さて、

$ $py36 -c 'import tensorflow; print("hello")'
hello

$ $py36 oreno.py
* Serving Flask app "server" (lazy loading)
* Environment: production
...
INFO:werkzeug: * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
...

ようやく正常に起動できました!!!

が、socket.getaddrinfo() が失敗して名前が引けず…

まとめ

  • ld-linux.so はコマンドとしても実行できて、 --library-path でdynamic loadするshared objectのパスを指定できる(環境変数 LD_LIBRARY_PATH に比べ作用範囲を限定的にできる)
  • ELFには、dynamic linker (interpreter) とライブラリのサーチパス(runpath)を埋め込むことができ、patchelf コマンドで変更できる

ところで、

debootstrap で bionic の環境を作ってまるっとCentOS 6にコピーして chroot した方がよかったかもしれん… 試してないけど動くんじゃないかと…

kernelが古くて残念、ダメでした!

# chroot ./bionic /bin/bash
FATAL: kernel too old

# file bionic/bin/bash
bionic/bin/bash: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 3.2.0, stripped