NixOS上でChainerのTutorialをビルドしてみた

# 使用するもの
## [NixOS](https://nixos.org)
- Linuxディストリビューションの１つ
  - 関数型パッケージ管理ツールNixを使用している
  - パッケージ自体は[nixpkgs](https://github.com/NixOS/nixpkgs)のリポジトリで管理・開発されている(個々のパッケージは[https://nixos.org/nixos/packages.html#](https://nixos.org/nixos/packages.html#) で検索できる)
- 環境の構築が楽にできる
  - １つのファイルでシステム全体の設定ができる -> 管理が楽
  - システムのアップデート -> システムのロールバック ということが楽にできる
  - sudoとかしなくてもパッケージを用意できる(たとえばpythonでnumpyとmatplotlibを使いたい時は以下のようにする)
 
```
$ nix-shell -p python3 -p pythonPackages.numpy -p pythonPackages.matplotlib
```

- NixOpsを使ってリモートに環境のデプロイができる

## [Chainer](https://chainer.org/)
- ニューラルネットワーク記述用のpythonフレームワーク

# はじめに
NixOS上でChainerを使いたい方がどれぐらいいるのかはわかりません。
しかしながら、これを書いている時点ではnixpkgsにはChainerが含まれていません。
そこでNixOSでchainerを使えるようにすることを目指し、
まずはnix(パッケージ管理ツールNixと関数型言語nix、ややこしい)でDerivationのためのコードを書いてみました。
今回は、Chainerのチュートリアルのコードを動かすことをゴールとします。

# ChainerのDerivationのためのコード
はじめてnixでのDerivationのコードを書く場合はTokyo-NixOS-Meetup[^1]の[このページ](https://github.com/Tokyo-NixOS/presentations/blob/master/2017/01/source.markdown)が参考になります。
[nixpkgs](https://github.com/NixOS/nixpkgs)の中身を見てコードを参考にするのも良いです。

とりあえず、以下のように書いてみました。
https://github.com/hyphon81/nix_code_for_building_chainer_and_cupy
※このコードはCUDAデバイスがある前提でビルドするようにできています。
(今回はそうしていませんが、nixでは環境によっての条件分岐を行うようなコードも書けます)

コードのディレクトリの構成は以下のようになっています。

```:ディレクトリ構成
nix_code_for_building_chainer_and_cupy/       # derivationのプロジェクト全体のディレクトリ

  chainer/                                    # chainerパッケージのderivationコードのディレクトリ
    default.nix                               # chainerのderivationコード

  cupy/                                       # cupyパッケージのderivationコードのディレクトリ
    default.nix                               # cupyのderivationコード

  fastrlock/                                  # fastrlockパッケージ(cupyのビルドに必要)のderivationコードのディレクトリ
    default.nix                               # fastrlockのderivationコード

  filelock/                                   # filelockパッケージ(chainerのビルドに必要)のderivationコードのディレクトリ
    default.nix                               # filelockのderivationコード

  nccl/                                       # nccl(multi gpuのために必要だが今の所使っていない)パッケージのderivationコードのディレクトリ
    default.nix                               # ncclのderivationコード
```
見てもらえばわかる通り、[パッケージ名]/default.nixという構成になっております。
default.nixは名前の通りデフォルトで呼び出させるファイルで、パスの指定でファイル名がない場合に読み込まれます。
index.htmlみたいなものです。

default.nixの中身はそんなに難しくはありません。
1番かんたんな例でfastrlockのnixのコードを見てみます。

```nix:fastrlock/default.nix
{ stdenv, python, fetchPypi }:

with python.pkgs;

buildPythonPackage rec {
  name = "${pname}-${version}";
  pname = "fastrlock";
  version = "0.3";

  src = fetchPypi {
    inherit pname version;
    sha256 = "00mr9b15d539z89ng5nf89s2ryhk90xwx95jal77ma0wslixrk5d";
  };
}
```

これだけでpypiからfastrlockのバージョン0.3をダウンロードしてきてビルド、インストールしてくれます。
sha256はダウンロードするファイルのハッシュです。
私はこのハッシュを知るのに、一度いいかげんなハッシュでダウンロード失敗させて出るエラーメッセージを見るんですが、
誰かもっといい方法をご存じの方がいたら教えてください・・・。
filelockも同じようにできます。

```nix:filelock/default.nix
{ stdenv, python, fetchPypi }:

with python.pkgs;

buildPythonPackage rec {
  name = "${pname}-${version}";
  pname = "filelock";
  version = "2.0.13";

  src = fetchPypi {
    inherit pname version;
    sha256 = "1n67dw7np5gsy5whynyk8c46pjlr353d6j9735p5gryaszkpjl6h";
  };
}
```

ncclはpythonのパッケージではないのでbuildPythonPackageではなくmkDerivationを使います。

```nix:nccl/default.nix
{ stdenv, pkgs, fetchurl }:

with pkgs;

stdenv.mkDerivation rec {
  name = "nccl-${version}";
  version = "1.3.4-1";

  src = fetchurl {
    url = "https://github.com/NVIDIA/nccl/archive/v${version}.tar.gz";
    sha256 = "117qz8zvc4r0zjd737knr482gf208v5xlwyrpaf8pcjvam2fpr0i";
  };

  nativeBuildInputs = [
    gcc5
    eject
  ];

  propagatedBuildInputs = [
    cudatoolkit8
  ];

  BUILDDIR = "./";
  PREFIX = "$out";
  CUDA_HOME = "${cudatoolkit8}";
  CUDA_LIB = "${cudatoolkit8.lib}/lib";

  postInstall = ''
    cp -r ./lib $out
    cp -r ./include $out
  '';
}
```

基本は、今までのものと変わらずsrcにビルドしたいパッケージの元になるファイルを持ってくるだけです。
ただ、ここではnativeBuildInputsとpropagatedBuildInputsというのが出てきます。
nativeBuildInputsにはビルド時にだけ必要なパッケージを渡します。
propagatedBuildInputsにはビルド後も必要になるパッケージを渡します。

BUILDDIR、PREFIX、CUDA_HOME、CUDA_LIBはビルド時に使う環境変数です。
postInstallはインストール時の処理(installPhase)の後にやるコマンドを記述します。
installPhaseでは環境変数の設定
ここでは、ビルドしてできた.soの共有ライブラリファイルを出力先のlibへ、
.hのヘッダファイルを出力先のincludeへコピーしています。
これやらないと、インストール後に肝心の.soと.hが無いという状態になってしまったので
仕方なく記述している感じです。
postInstallがなくても、Makefileを見る限りうまく行くと思っていたんですが、そう簡単にはいきませんでした。
しかしながら、(あまりかっこよくはないかも知れませんが)これでビルドできます。

次にcupyです。

```nix:cupy/default.nix
{ stdenv,
  pkgs,
  python,
  fetchPypi
}:

with pkgs;
with python.pkgs;

let
  fastrlock = callPackage ../fastrlock {
    python = python;
  };
  nccl = callPackage ../nccl {};
in

buildPythonPackage rec {
  name = "${pname}-${version}";
  pname = "cupy";
  version = "2.2.0";

  src = fetchPypi {
    inherit pname version;
    sha256 = "0si0ri8azxvxh3lpm4l4g60jf4nwzibi53yldbdbzb1svlqq060r";
  };

  nativeBuildInputs = [
    gcc5
  ];

  propagatedBuildInputs = [
    cudatoolkit8
    cudnn60_cudatoolkit80
    linuxPackages.nvidia_x11
    nccl
    fastrlock
    numpy
    six
    wheel
  ];

  CUDA_PATH = "${cudatoolkit8}";
  CFLAGS = "-I ${cudnn60_cudatoolkit80}/include -I ${nccl}/include";
  LDFLAGS = "-L ${cudnn60_cudatoolkit80}/lib -L ${nccl}/lib";

  # In python3, test was failed...
  doCheck = isPy27;
}
```
新しく let ... in、callPackage、doCheckが出てきました。

let ... inですが、この中に書かれた変数はそれ以下で使用できます。
callPackageでは使用するパッケージを取得しています。
fastrlock/default.nixとnccl/default.nixで用意したパッケージを取得します。
ここでfastrlock/default.nixは関数として扱われるのでpython = pythonとします。
これはpython2.7やpython3.6などpythonのバージョンに合わせてパッケージをビルドするためです。

doCheckはテストをするかどうかのフラグなのですが、
どうもpython3だとテストで引っかかってしまうので、
しかたなくpython2.7の時のみテストをします。

肝心のchainerは以下のようになっています。
cupyとあまり変わりません。

```nix:chainer/default.nix
{ stdenv,
  pkgs,
  python,
  fetchPypi
}:

with pkgs;
with python.pkgs;

let
  cupy = callPackage ../cupy {
    python = python;
  };
  filelock = callPackage ../filelock {
    python = python;
  };
in

buildPythonPackage rec {
  name = "${pname}-${version}";
  pname = "chainer";
  version = "3.2.0";

  src = fetchPypi {
    inherit pname version;
    sha256 = "0mbc8kwk7pvg03bf0j57a48gr6rsdg4lzmyj0dak8y2l4lmyskpw";
  };

  propagatedBuildInputs = [
    cupy
    filelock
    protobuf3_3
  ];

  # In python3, test was failed...
  doCheck = isPy27;
}
```

とりあえず、このようにchainerのビルド用のコードを用意しました。

# ChainerのTutorialのビルド
chainerのビルド用のコードができたので、うまく動くか確認がしたいです。
テストのために、公式のドキュメント[^2]をもとにchainerのチュートリアルのコードを書いてみました。

```python3:tutorial.py
import chainer
import chainer.functions as F
import chainer.links as L
from chainer import training, datasets, iterators, optimizers
from chainer import Chain
from chainer.training import extensions
import numpy as np

uses_device = 0


class MLP(Chain):


    def __init__(self, n_units, n_out):
        super(MLP, self).__init__()
        with self.init_scope():
            # the size of the inputs to each layer will be inferred
            self.l1 = L.Linear(None, n_units) # n_in -> n_units
            self.l2 = L.Linear(None, n_units) # n_units -> n_units
            self.l3 = L.Linear(None, n_out)   # n_units -> n_out

    def __call__(self, x):
        h1 = F.relu(self.l1(x))
        h2 = F.relu(self.l2(h1))
        y = self.l3(h2)
        return y


class Classifier(Chain):


    def __init__(self, predictor):
        super(Classifier, self).__init__()
        with self.init_scope():
            self.predictor = predictor


    def __call__(self, x, t):
        y = self.predictor(x)
        loss = F.softmax_cross_entropy(y, t)
        accuracy = F.accuracy(y, t)
        report({'loss': loss, 'accuracy': accuracy}, self)
        return loss


def main():


    model = L.Classifier(MLP(100, 10)) # the input size, 784, is inferred

    if uses_device >= 0:
        # use GPU
        chainer.cuda.get_device_from_id(0).use()
        chainer.cuda.check_cuda_available()
        # transform data for GPU
        model.to_gpu()

    train, test = datasets.get_mnist()

    train_iter = iterators.SerialIterator(train, batch_size=100, shuffle=True)
    test_iter = iterators.SerialIterator(test, batch_size=100, repeat=False, shuffle=False)

    optimizer = optimizers.SGD()
    optimizer.setup(model)

    updater = training.StandardUpdater(train_iter, optimizer, device=uses_device)
    trainer = training.Trainer(updater, (20, 'epoch'), out='output')

    trainer.extend(extensions.Evaluator(test_iter, model, device=uses_device))
    trainer.extend(extensions.LogReport())
    trainer.extend(extensions.PrintReport(['epoch', 'main/accuracy', 'validation/main/accuracy', 'elapsed_time']))
    trainer.extend(extensions.ProgressBar())

    trainer.run()


if __name__ == '__main__':
    main()
```

ここで、nixのbuildPythonApplicationでビルドを行うにはsetup.pyを記述する必要がありました。
この時点で私はpythonのパッケージングの経験がなかったので、方法を検索して出てきたページ[^3] [^4]を参考に、
setup.pyを記述しました。

```:ディレクトリ構成
nix_code_for_building_chainer_and_cupy/      # chainerのderivation用nixコード

tutorial/                                    # tutorialビルド用のパッケージ全体
  default.nix                                # tutorialのコードのビルド用のnixコード
  src/                                       # ソースコードのディレクトリ
    setup.py                                 # pythonパッケージのビルド用のコード
    codes/                                   # ビルドするpythonパッケージのソースコード
      __init__.py                            # 空のファイル
      tutorial.py                            # tutorialのソースコード
```

```python:src/setup.py
from distutils.core import setup

setup(
    name='tutorial',
    version='0.0.1',
    description='A tutorial of the chainer',
    author='hyphon81',
    author_email='hyphon81@example.com',
    install_requires=[
        'numpy',
        'cupy',
        'chainer'
    ],
    dependency_links=[],
    packages=['codes'],
    entry_points="""
    [console_scripts]
    tutorial = codes.tutorial:main
    """,
)
```

あとは、ディレクトリ構成に示すように、srcと同じ階層にdefault.nixを用意します。

```nix:tutorial/default.nix
with import <nixpkgs> {};

with pkgs;
with python3Packages;

let
  chainer = callPackage ../nix_code_for_building_chainer_and_cupy/chainer {
    python = python3;
  };
in

buildPythonApplication rec {
  name = "chainer-tutorial";

  src = ./src;

  buildInputs = [
    makeWrapper
  ];

  propagatedBuildInputs = [
    cudatoolkit8
    chainer
  ];

  postInstall = ''
    wrapProgram $out/bin/tutorial \
      --prefix LD_LIBRARY_PATH : ${cudatoolkit8}/lib
  '';
}
```

ここでcudatoolkitとmakeWrapperが必要なのは、
tutorialのコード実行時にLD_LIBRARY_PATHにcudatoolkitのlibが入っていないとnvrtのエラーが出るためです。

ファイルがすべて用意できた後、tutorial/default.nixがある階層でnix-buildコマンドを実行します

```
$ cd tutorial
$ nix-build
```

必要なファイルのダウンロードやビルドが始まり、エラーが出なければ./result/bin/tutorialができるはずです。
私の環境ではCUDAデバイスの利用にoptirunを使うので以下のように実行します。

```
$ optirun ./result/bin/tutorial
epoch       main/accuracy  validation/main/accuracy  elapsed_time
1           0.659433       0.8579                    11.9421       
2           0.873484       0.8949                    14.3572       
3           0.895317       0.9072                    16.6135       
4           0.905051       0.9145                    18.9283       
5           0.911968       0.9212                    21.414        
6           0.917368       0.9247                    23.7878       
     total [###############...................................] 31.67%
this epoch [################..................................] 33.33%
```

こんな感じになります。

# まとめ
今回は、NixOS上でChainerを使用するために、derivation用のnixコードを作成しました。
作成したものを使い、tutorial用のコードをパッケージングしてビルドし動作させました。

# 参考文献
[^1]: [Tokyo-NixOS-Meetup-Github](https://github.com/Tokyo-NixOS/presentations)
[^2]: [Chainer Tutorial](https://docs.chainer.org/en/stable/tutorial/basic.html#example-multi-layer-perceptron-on-mnist)
[^3]: [（インターン向けに書いた）Pythonパッケージを作る方法](https://qiita.com/Kensuke-Mitsuzawa/items/7717f823df5a30c27077)
[^4]: [Python の setup.py を書いてみる](http://momijiame.tumblr.com/post/39580970098/python-%E3%81%AE-setuppy-%E3%82%92%E6%9B%B8%E3%81%84%E3%81%A6%E3%81%BF%E3%82%8B)