tensorflow-1.5.1を使いたい。nix-shellで。

# 背景

古いPCを使用して機械学習をしようと思ったのですが、
TensorFlowのversion 1.6以降からはCPUのAVX命令を必ず使用するようになっており、
これが古いCPUでversion 1.6以降のTensorFlowを使えなくするようです。
https://github.com/tensorflow/tensorflow/releases/tag/v1.6.0

この制約に引っかかってKerasのbackendで新しいTensorflowが使えず、
TensorFlowのversion 1.5.1を使いたい状況になったのですが、
NixOSを使用している都合上、少し面倒だったので記録を残します。

# pip? 残念ながら使用できません。

pip使えばいいじゃんと思うかもしれませんが、
NixOSのパッケージマネージャであるnixの制約上、パッケージデータはすべて/nix/store上で管理され、
パッケージの管理機構が/nix/store以外のディレクトリへ何かを書き込むことは固く禁じられています。
そしてこの条件でpipをインストールした場合、pipの実行ファイルや見ている/libなどのパスは、/nix/store配下にあります。
さらに、/nix/storeはnixパッケージマネージャ以外書き込みを許されていません。
こうなるとpipがパッケージをインストールする際に書き込もうとするのは/nix/storeの配下のどこかになるのですが、
pipにはそのパスへの書き込み権限がないことになります。
つまり、ひとことで言うとpipは使用できません。
(やろうとすると、書き込み権限がないというエラー文が見られると思います。)

nixではnixの流儀に従ってパッケージをインストールして環境を用意する必要があります。

# NixOSでパッケージをインストールする方法

NixOSでパッケージをインストールして使用する方法は大きく3つあります。

### 1. /etc/nixos/configuration.nixのenvironment.systemPackagesにパッケージを追加する

システム全体、すべてのユーザでパッケージを使用したい場合はこの方法をとります。

### 2. nix-envでパッケージをインストールする

ユーザ固有でパッケージをインストールする場合にこの方法をとります。

### 3. nix-shellでパッケージがインストールされた環境を用意する

その場だけで使用する環境を用意したいという場合は、この方法が便利です。
たとえば、同じマシンでCUDAのversion 9.0とversion 10.0の両方を使用したい場合などは、
コンソールを2つ開いて以下のようにすれば、それぞれ独立して異なるバージョンのCUDAを使用できます。

- コンソール1

```shell-session
$ nix-shell -p cudatoolkit_9_0

[nix-shell:~]$ nvcc --version
nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2017 NVIDIA Corporation
Built on Fri_Sep__1_21:08:03_CDT_2017
Cuda compilation tools, release 9.0, V9.0.176
```

- コンソール2

```shell-session
$ nix-shell -p cudatoolkit_10_0

[nix-shell:~]$ nvcc --version
nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2018 NVIDIA Corporation
Built on Sat_Aug_25_21:08:01_CDT_2018
Cuda compilation tools, release 10.0, V10.0.130
```

このようにして、コンソール毎にインスタントに環境を用意できます。
今回はこの方法を利用してTensorFlow version 1.5.1の環境を用意します。

# Tensorflow? 新しいバージョンならあるよ

これを書いている時点で、NixOSの最新バージョンは19.03になっており、
https://nixos.org で確認できるのはnixパッケージマネージャにはTensorFlowのversion 1.13.1が用意されていることでした。
https://nixos.org/nixos/packages.html#tensorflow

古いバージョンのパッケージは残っていないようでした。

# overrideとoverrideDerivationによるTensorFlowのダウングレード

そこでoverrideDerivationの出番です。
nixではoverrideやoverrideDerivationを使用して、
nixパッケージマネージャ上で管理されているパッケージのビルドの仕方を記述したファイル(ここではDerivationファイルと呼ぶ)の内容を上書きできます。

Derivationファイルの内容は https://github.com/NixOS/nixpkgs で見ることができます。
TensorFlowのDerivationファイルはこれを書いている時点で以下のようになっていました。

```nix
#### ここから引数 ####
{ stdenv
, lib
, fetchurl
, buildPythonPackage
, isPy3k, isPy36, pythonOlder
, astor
, gast
, numpy
, six
, termcolor
, protobuf
, absl-py
, grpcio
, mock
, backports_weakref
, enum34
, tensorflow-estimator
, tensorflow-tensorboard
, cudaSupport ? false
, cudatoolkit ? null
, cudnn ? null
, nvidia_x11 ? null
, zlib
, python
, symlinkJoin
, keras-applications
, keras-preprocessing
}:
#### ここまで引数 ####

# We keep this binary build for two reasons:
# - the source build doesn't work on Darwin.
# - the source build is currently brittle and not easy to maintain

assert cudaSupport -> cudatoolkit != null
                   && cudnn != null
                   && nvidia_x11 != null;
let
  cudatoolkit_joined = symlinkJoin {
    name = "unsplit_cudatoolkit";
    paths = [ cudatoolkit.out
              cudatoolkit.lib ];};

in buildPythonPackage rec {
  pname = "tensorflow";
  version = "1.13.1";
  format = "wheel";

#### ここでインストールするTensorFlowのソースファイルをダウンロードする ####
#### ソースファイルの指定は別ファイル tf1.13.1-hashes.nixに記述されている ####
  src = let
    pyVerNoDot = lib.strings.stringAsChars (x: if x == "." then "" else x) "${python.pythonVersion}";
    pyver = if stdenv.isDarwin then builtins.substring 0 1 pyVerNoDot else pyVerNoDot;
    platform = if stdenv.isDarwin then "mac" else "linux";
    unit = if cudaSupport then "gpu" else "cpu";
    key = "${platform}_py_${pyver}_${unit}";
    dls = import (./. + "/tf${version}-hashes.nix");
  in fetchurl dls.${key};
########

  propagatedBuildInputs = [  protobuf numpy termcolor grpcio six astor absl-py gast tensorflow-estimator tensorflow-tensorboard keras-applications keras-preprocessing ]
                 ++ lib.optional (!isPy3k) mock
                 ++ lib.optionals (pythonOlder "3.4") [ backports_weakref ];

  # Upstream has a pip hack that results in bin/tensorboard being in both tensorflow
  # and the propageted input tensorflow-tensorboard which causes environment collisions.
  # another possibility would be to have tensorboard only in the buildInputs
  # https://github.com/tensorflow/tensorflow/blob/v1.7.1/tensorflow/tools/pip_package/setup.py#L79
  postInstall = ''
    rm $out/bin/tensorboard
  '';

  # Note that we need to run *after* the fixup phase because the
  # libraries are loaded at runtime. If we run in preFixup then
  # patchelf --shrink-rpath will remove the cuda libraries.
  postFixup = let
    rpath = stdenv.lib.makeLibraryPath
      ([ stdenv.cc.cc.lib zlib ] ++ lib.optionals cudaSupport [ cudatoolkit_joined cudnn nvidia_x11 ]);
  in
  lib.optionalString (stdenv.isLinux) ''
    rrPath="$out/${python.sitePackages}/tensorflow/:$out/${python.sitePackages}/tensorflow/contrib/tensor_forest/:${rpath}"
    internalLibPath="$out/${python.sitePackages}/tensorflow/python/_pywrap_tensorflow_internal.so"
    find $out -name '*${stdenv.hostPlatform.extensions.sharedLibrary}' -exec patchelf --set-rpath "$rrPath" {} \;
  '';


  meta = with stdenv.lib; {
    description = "Computation using data flow graphs for scalable machine learning";
    homepage = http://tensorflow.org;
    license = licenses.asl20;
    maintainers = with maintainers; [ jyp abbradar ];
    platforms = with platforms; linux ++ lib.optionals (!cudaSupport) darwin;
    # Python 2.7 build uses different string encoding.
    # See https://github.com/NixOS/nixpkgs/pull/37044#issuecomment-373452253
    broken = stdenv.isDarwin && !isPy3k;
  };
}
```

- tf1.13.1-hashes.nix

```nix
#### 以下にはソースファイルのダウンロード元とsha256ハッシュが記述されている ####
{
linux_py_27_cpu = {
  url = "https://storage.googleapis.com/tensorflow/linux/cpu/tensorflow-1.13.1-cp27-none-linux_x86_64.whl";
  sha256 = "0y1vd3y5fxcjj5d35qbk8482b0s642nyp0c2sm068vx5wd4sjpcg";
};
linux_py_35_cpu = {
  url = "https://storage.googleapis.com/tensorflow/linux/cpu/tensorflow-1.13.1-cp35-cp35m-linux_x86_64.whl";
  sha256 = "0b27swk4c2vaimwzbzl4c7xnccr9cfak5a3848lfqlcavcmbp94j";
};
linux_py_36_cpu = {
  url = "https://storage.googleapis.com/tensorflow/linux/cpu/tensorflow-1.13.1-cp36-cp36m-linux_x86_64.whl";
  sha256 = "087jwjby3bym09z55cjhc587aasf01y6l009p1q2vcpfq7s7ljmk";
};
linux_py_37_cpu = {
  url = "https://storage.googleapis.com/tensorflow/linux/cpu/tensorflow-1.13.1-cp37-cp37m-linux_x86_64.whl";
  sha256 = "0as68dp87lh7ffcccb149km6vws15ap04604irxwz35fq9h7grxg";
};
linux_py_27_gpu = {
  url = "https://storage.googleapis.com/tensorflow/linux/gpu/tensorflow_gpu-1.13.1-cp27-none-linux_x86_64.whl";
  sha256 = "0bf239f2bnsbqs3qh4xdql9pgbsm0zk7j8q1hg0wn0wrq440n0ds";
};
linux_py_35_gpu = {
  url = "https://storage.googleapis.com/tensorflow/linux/gpu/tensorflow_gpu-1.13.1-cp35-cp35m-linux_x86_64.whl";
  sha256 = "1cqav22a8yz6fzk46z6kv1ha2i28h5wccbd7k66drrfxibmb93j0";
};
linux_py_36_gpu = {
  url = "https://storage.googleapis.com/tensorflow/linux/gpu/tensorflow_gpu-1.13.1-cp36-cp36m-linux_x86_64.whl";
  sha256 = "1xnbiz36z7nicqrv0cmymfnwb8mdz2hifcv71gh6gnyi1962f2d7";
};
linux_py_37_gpu = {
  url = "https://storage.googleapis.com/tensorflow/linux/gpu/tensorflow_gpu-1.13.1-cp37-cp37m-linux_x86_64.whl";
  sha256 = "10gcrmd9y5a89wpi4rpp9scc9l2krijv8yjp7iphlykmn54ps74k";
};
mac_py_2_cpu = {
  url = "https://storage.googleapis.com/tensorflow/mac/cpu/tensorflow-1.13.1-py2-none-any.whl";
  sha256 = "1a6y5xj2wqkd8qmabn2xjg3q7x2jfixwrz351dgcxlhy8qy5yc0g";
};
mac_py_3_cpu = {
  url = "https://storage.googleapis.com/tensorflow/mac/cpu/tensorflow-1.13.1-py3-none-any.whl";
  sha256 = "1klsv18k0isfd61z1wirfz1lnqmx8k73ga8g9s18yand65iycads";
};
}
```

Derivationファイルにはパッケージをビルドするための1つの関数が記述されており、
Derivationファイルの上部は関数へ渡す引数で、その下に関数本体が記述されています。
overrideはDerivationファイルが記述する関数に対して引数の内容を指定することができ、
overrideDerivationは関数の内容を上書きできます。

今回は、TensorFlowのバージョンを1.13.1から1.5.1にしたいということなので、
overrideDerivationを使用して、ビルドに使用するソースファイルを1.13.1のものから1.5.1のものへ書き換えればよさそうです。

ということで、以下のようにしてresouce1.shというファイルを用意して実行してみました。

- resource1.sh

```shell
nix-shell -p python36 \
          -p python36Packages.pandas \
          -p python36Packages.numpy \
          -p python36Packages.scikitlearn \
          -p python36Packages.Keras \
          -p "python36Packages.tensorflowWithCuda.overrideDerivation (attrs: {
                name = \"tensorflow-1.5.1\";
                version = \"1.5.1\";
                src = fetchurl {
                  url = \"https://storage.googleapis.com/tensorflow/linux/gpu/tensorflow_gpu-1.5.1-cp36-cp36m-linux_x86_64.whl\";
                  sha256 = \"1d26ndgkm6qi39cb8zkd7apnxkg2h5s634mnh53rdw1xmq3hrj2c\";
                };
              })"
```

overrideやoverrideDerivationを使用する場合は""でくくると、nixの構文として処理されます。
また、NixOS 19.03のデフォルトのPythonのバージョンは3.7になっていますが、
TensorFlow version 1.5.1のwheelのソースファイルは参照先ではPython 3.6に対応したバージョンまでしかなかったため、
python36を指定しています。
また、pandas、numpy、scikit-learnやKerasのパッケージもインストールします。
このファイルを実行すると、以下のようになりました。

```shell-session
$ ./resource1.sh

...

Collecting tensorflow-tensorboard<1.6.0,>=1.5.0 (from tensorflow-gpu==1.5.1)
  Could not find a version that satisfies the requirement tensorflow-tensorboard<1.6.0,>=1.5.0 (from tensorflow-gpu==1.5.1) (from versions: )
No matching distribution found for tensorflow-tensorboard<1.6.0,>=1.5.0 (from tensorflow-gpu==1.5.1)
builder for '/nix/store/kh39ldzwxszrazj9qphz3pznvigg2nrk-tensorflow-1.5.1.drv' failed with exit code 1
error: build of '/nix/store/kh39ldzwxszrazj9qphz3pznvigg2nrk-tensorflow-1.5.1.drv' failed
```

tensorflow-tensorboardのバージョンが1.5.0以上、1.6.0未満という条件を満たしていないと怒られました。

この場合、tensorflowのDerivationファイルにある引数tensorflow-tensorboardの値をoverrideで上書きすればよさそうです。

- resource2.sh

```shell
nix-shell -p python36 \
          -p python36Packages.pandas \
          -p python36Packages.numpy \
          -p python36Packages.scikitlearn \
          -p python36Packages.Keras \
          -p "(python36Packages.tensorflowWithCuda.override {
                tensorflow-tensorboard = python36Packages.tensorflow-tensorboard.overrideDerivation (attrs: {
                  name = \"tensorflow-tensorboard-1.5.1\";
                  src = python36Packages.fetchPypi {
                    pname = \"tensorflow_tensorboard\";
                    version = \"1.5.1\";
                    format = \"wheel\";
                    python = \"py3\";
                    sha256 = \"1cydgvrr0s05xqz1v9z2wdiv60gzbs8wv9wvbflw5700a2llb63l\";
                  };
                });
              }).overrideDerivation (attrs: {
                name = \"tensorflow-1.5.1\";
                version = \"1.5.1\";
                src = fetchurl {
                  url = \"https://storage.googleapis.com/tensorflow/linux/gpu/tensorflow_gpu-1.5.1-cp36-cp36m-linux_x86_64.whl\";
                  sha256 = \"1d26ndgkm6qi39cb8zkd7apnxkg2h5s634mnh53rdw1xmq3hrj2c\";
                };
              })"
```

構造としては、先ほどのresource1.shでやったoverrideDerivationに対してoverrideでtensorflow-tensorboardの値を
バージョンが1.5.1のソースファイルを使用したtensorflow-tensorboardのパッケージに上書きしたものを渡した形になります。
そして、tensorflow-tensorboardのパッケージはoverrideDerivationでソースファイルを1.5.1のものに指定しています。
これを実行してみます。

```shell-session
$ ./resource2.sh

...

Collecting bleach==1.5.0 (from tensorflow-tensorboard==1.5.1)
  Could not find a version that satisfies the requirement bleach==1.5.0 (from tensorflow-tensorboard==1.5.1) (from versions: )
No matching distribution found for bleach==1.5.0 (from tensorflow-tensorboard==1.5.1)
builder for '/nix/store/nqmammii264bf0z75n681g9qh2qa4l59-tensorflow-tensorboard-1.5.1.drv' failed with exit code 1
cannot build derivation '/nix/store/jlvypi8xvqccy6fv82a3l76p2i04aa80-tensorflow-1.5.1.drv': 1 dependencies couldn't be built
error: build of '/nix/store/jlvypi8xvqccy6fv82a3l76p2i04aa80-tensorflow-1.5.1.drv' failed
```

tensorflow-tensorboardのビルド中にbleachの1.5.0が無いと言って失敗しました。
なのでtensorflow-tensorboardのoverrideDerivationの中で、bleachの1.5.0のパッケージを使用するように書き換えればよいです。

こんなことを繰り返し行い、最終的には以下の形になりました。

- resource3.sh

```shell
nix-shell -p python36 \
          -p python36Packages.pandas \
          -p python36Packages.numpy \
          -p python36Packages.scikitlearn \
          -p python36Packages.Keras \
          -p "(python36Packages.tensorflowWithCuda.override {
                cudatoolkit = pkgs.cudatoolkit_9_0;
                cudnn = pkgs.cudnn_cudatoolkit_9_0;
                tensorflow-tensorboard = python36Packages.tensorflow-tensorboard.overrideDerivation (attrs: {
                  name = \"tensorflow-tensorboard-1.5.1\";
                  src = python36Packages.fetchPypi {
                    pname = \"tensorflow_tensorboard\";
                    version = \"1.5.1\";
                    format = \"wheel\";
                    python = \"py3\";
                    sha256 = \"1cydgvrr0s05xqz1v9z2wdiv60gzbs8wv9wvbflw5700a2llb63l\";
                  };
                  propagatedBuildInputs = attrs.propagatedBuildInputs ++ [
                    ((python36Packages.bleach.override {
                      html5lib = ((python36Packages.html5lib.override {
                        pytest = python36Packages.pytest_3;
                      }).overrideDerivation (attrs: {
                        name = \"html5lib-0.9999999\";
                        src = python36Packages.fetchPypi {
                          pname = \"html5lib\";
                          version = \"0.9999999\";
                          sha256 = \"0s28fp85hizzik3hh94x84ibgp5rpd8blhby0px2p16mm28s24i6\";
                        };
                        preInstall = ''
                          rm html5lib/tests/testdata/encoding/chardet/test_big5.txt
                          rm html5lib/tests/test_treewalkers.py
                          rm html5lib/tests/test_tokenizer.py
                          rm html5lib/tests/test_serializer.py
                          rm html5lib/tests/test_sanitizer.py
                          rm html5lib/tests/test_parser.py
                        '';
                      }));
                      pytest = (python36Packages.pytest_3.overrideDerivation (attrs: {
                        name = \"pytest-3.0.3\";
                        src = python36Packages.fetchPypi {
                          pname = \"pytest\";
                          version = \"3.0.3\";
                          sha256 = \"1rxydacrdb8s312l3bn0ybrqsjp13abzyim1x21s80386l5504zj\";
                        };
                        preInstall = ''
                          rm testing/test_capture.py
                          rm testing/test_doctest.py
                        '';
                        doCheck = false;
                      }));
                    }).overrideDerivation (attrs: {
                      name = \"bleach-1.5.0\";
                      src = python36Packages.fetchPypi {
                        pname = \"bleach\";
                        version = \"1.5.0\";
                        sha256 = \"0rdwb3piwwl30wfqg4ywm07276w7090xfq71lb5d6k5mk62pb3lp\";
                      };
                    }))
                  ];
                });
              }).overrideDerivation (attrs: {
                name = \"tensorflow-1.5.1\";
                version = \"1.5.1\";
                src = fetchurl {
                  url = \"https://storage.googleapis.com/tensorflow/linux/gpu/tensorflow_gpu-1.5.1-cp36-cp36m-linux_x86_64.whl\";
                  sha256 = \"1d26ndgkm6qi39cb8zkd7apnxkg2h5s634mnh53rdw1xmq3hrj2c\";
                };
              })"
```

ずいぶんと長くなってしまいましたが、これを実行するとちゃんとインストールが成功しました。

```shell-session
$ ./resource3.sh

...

[nix-shell:~]$ python
Python 3.6.8 (default, Dec 24 2018, 03:01:30)
[GCC 7.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import tensorflow as tf
>>> print(tf.__version__)
1.5.1
>>>
```

途中、いくつかのPythonのパッケージのインストール中に、DerivationのinstallPhaseでtestが動く場合があり、
そのtestに失敗するので、失敗するtestのファイルはpreInstallで消去する対応をしているため、
あまりよくないかもしれません。
bleach 1.5.0のビルドではhtml5libとpytestのバージョンを細かく指定されたため、
指定されたバージョンに合うソースファイルを使用しています。
また、1度ビルドが通ってTensorFlow 1.5.1を使用する際、libcublas.so.9.0が見つからないというエラーが出たため、
使用するCUDAのバージョンは9.0に指定しています。

今回は以上です。