使用するもの
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のコードを見てみます。
{ 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も同じようにできます。
{ 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を使います。
{ 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です。
{ 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とあまり変わりません。
{ 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のチュートリアルのコードを書いてみました。
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のソースコード
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を用意します。
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用のコードをパッケージングしてビルドし動作させました。