LoginSignup
5
3

More than 5 years have passed since last update.

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

Posted at

使用するもの

NixOS

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

Chainer

  • ニューラルネットワーク記述用のpythonフレームワーク

はじめに

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

ChainerのDerivationのためのコード

はじめてnixでのDerivationのコードを書く場合はTokyo-NixOS-Meetup1このページが参考になります。
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のコードを見てみます。

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も同じようにできます。

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を使います。

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です。

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とあまり変わりません。

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のチュートリアルのコードを書いてみました。

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のソースコード
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を用意します。

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用のコードをパッケージングしてビルドし動作させました。

参考文献

5
3
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3