WorkQueue Model with POSIX Threads

少し前の記事で書きましたが、MNIST を MDS で処理する計算は、全て Python で書いたせいかパフォーマンスが悪く、N=10000 のデータにも関わらず N=200 程度で 6 分もかかってしまい、話になりませんでした。

MNIST with MDS in Python | すなのかたまり
https://msmania.wordpress.com/2015/12/13/mnist-with-mds-in-python/

しかし、ここまでやってようやく気づいてしまったのは、Field クラスは C++ で書いてしまったほうがよかったかもしれない、ということ。Sandy Bridge だから行列計算に AVX 命令を使えば高速化できそう。というか Python の SciPy は AVX 使ってくれないのかな。ノードにかかる運動ベクトルをラムダ式のリストにしてリスト内包表記で計算できるなんてエレガント・・という感じだったのに。

そこで、AVX 命令を使って演算するモジュールを C++ で書いて Boost.Python 経由で読み込むように書き換えてみました。(ばねの力を計算する部分で少しアルゴリズムも少し変えています。) 結果、期待していたよりもパフォーマンスが上がり、N=1000 でも 1 分弱、N=10000 の場合でも 1 時間程度で結果を得ることができました。

パラメーターをいろいろ変えて試しているので動きが統一されていませんが、結果はそれなりに統一されています。例えば、青色 (=1) のデータは、データ間の距離がかなり短いらしく収束が早いことがすぐに分かります。逆に、灰色 (= 8) や白色 (= 7) のデータは他の数字と混じっており、ユークリッド距離だけだと区別が難しそうです。緑 (= 4) と紺色 (= 9) は同じエリアを共有しがちですが、確かに数字の形を考えると、似ていると言えなくもありません。。

http://ubuntu-web.cloudapp.net/ems/cmodule.htm

image

結果の考察はさておいて、もう少し欲を出してみたくなりました。そこで試みたのは 2 点。

  1. ノードを動かす前に、各ノード間をばねで繋ぐ際のユークリッド距離の計算が遅い
  2. オイラー法の計算の並列化

1. は未だ Python でやっており、N=10000 の場合だと、C(10000, 2) = 49,995,000 回も 784 次元座標のユークリッド距離を計算していることになるので、馬鹿にならない時間がかかります。入力データは変わらないので、予め全部の距離を計算してファイルに書き出しておくことにしました。おそらく、ファイルを読むだけの方が距離を計算するのよりは早いはずです。

2. はそのままマルチスレッド化です。1 時間も 1 CPU だけが仕事をしているのは何かもったいないので、何とか並列化したいところ。アイディアとしては、扱っている 2 次元データを次元毎にスレッドを分けて計算したい、というものです。

結果から書くと、1 は成功、2 は失敗です。1. だけ実装して、N=10000 で 1 時間を切ることはできました。

計算の大部分を占めるのが、Field::Move で、最初にばねの力を足し合わせるためのループから Field::Spring::load を呼んでいます。次元毎に処理を分けるのは Move() 後半のループで、コード自体は簡単に書けましたが、パフォーマンスはほぼ同じか、少し悪くなりました。前半のばねの計算のところでは、ばね毎の計算を分けて各スレッドに割り振ってみましたが、こちらのパフォーマンスは最悪で、2 倍以上落ちました。

void Field::Spring::load() {
    std::valarray<double> diff(_field->_dim);
    int i, n = _field->_m.size();
    double norm = .0;
    for (i = 0 ; i < _field->_dim ; ++i) {
        diff[i] = _field->_position[_n2 + i * n] – _field->_position[_n1 + i * n];
        norm += diff[i] * diff[i];
    }
    norm = sqrt(norm);
    for (i = 0 ; i < _field->_dim ; ++i) {
        double f = _k * (1 – _l / norm) * diff[i];
        _field->_accel[_n1 + n * i] += f / _field->_m[_n1];
        _field->_accel[_n2 + n * i] += -f / _field->_m[_n2];
    }
}

void Field::Move(double dt) {
    int i, j, n = _m.size();
    _accel = .0;
    for (auto &f : _forces) {
        if (f != nullptr) {
            f->load(); <<<< この呼び出しを WorkItem にする
        }
    }
    for (i = 0 ; i < _dim ; ++i) {
        for (j = 0 ; j < n ; ++j) { <<<< このループを WorkItem にする
            int idx = j + i * n;
            _velocity[idx] += dt * _accel[idx];
            _velocity[idx] -= _velocity[idx] * _friction[j] / _m[j];
            _position[idx] += dt * _velocity[idx];
        }
    }
}

コードは dev-mt ブランチにまとめています。コミットの順番を間違えた上、マージのやり方も微妙でコミット履歴がぐちゃぐちゃ・・

msmania/ems at dev-mt · GitHub
https://github.com/msmania/ems/tree/dev-mt

Linux でマルチスレッドのプログラムをまともに書いたことがなかったので、最善か分かりませんが、Windows でよくある、WorkQueue/WorkItem モデル (正式名称が分からない) を POSIX Thread を使って一から書きました。もしかすると標準や Boost に既にいいのがありそうですが。

ヘッダーの workq.h

class WorkQueue {
public:
    class Job {
    public:
        virtual ~Job() {}
        virtual void Run() = 0;
    };

private:
    enum WorkItemType {
        wiRun,
        wiExit,
        wiSync
    };

    class Event {
    private:
        pthread_mutex_t _lock;
        pthread_cond_t _cond;
        int _waitcount;
        int _maxcount;

    public:
        Event(int maxcount);
        virtual ~Event();
        void Wait();
    };

    class WorkItem {
    private:
        WorkItemType _type;
        void *_context;

    public:
        WorkItem(WorkItemType type, void *context);
        virtual ~WorkItem();
        virtual void Run();
    };

    int _numthreads;
    std::vector<pthread_t> _threads;
    Event _sync;
    pthread_mutex_t _taskqlock;
    std::queue<WorkItem*> _taskq;

    static void *StartWorkerthread(void *p);
    void *Workerthread(void *p);

public:
    WorkQueue(int numthreads);
    virtual ~WorkQueue();
    int CreateThreads();
    void JoinAll();
    void AddTask(Job *job);
    void Sync();
    void Exit();
};

ソースの workq.cpp

#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <queue>
#include "workq.h"

#ifdef _LOG
#include <stdio.h>
#define LOGINFO printf
#else
#pragma GCC diagnostic ignored "-Wunused-value"
#define LOGINFO
#endif

WorkQueue::Event::Event(int maxcount) : _waitcount(0), _maxcount(maxcount) {
    pthread_mutex_init(&_lock, nullptr);
    pthread_cond_init(&_cond, nullptr);
}

WorkQueue::Event::~Event() {
    pthread_cond_destroy(&_cond);
    pthread_mutex_destroy(&_lock);
}

// Test Command: for i in {1..1000}; do ./t; done
void WorkQueue::Event::Wait() {
    LOGINFO("[%lx] start:wait\n", pthread_self());
    pthread_mutex_lock(&_lock);
    if (__sync_bool_compare_and_swap(&_waitcount, _maxcount, 0)) {
        pthread_cond_broadcast(&_cond);
    }
    else {
        __sync_fetch_and_add(&_waitcount, 1);
        pthread_cond_wait(&_cond, &_lock);
    }
    pthread_mutex_unlock(&_lock);
}

WorkQueue::WorkItem::WorkItem(WorkItemType type, void *context)
    : _type(type), _context(context) {}

WorkQueue::WorkItem::~WorkItem() {}

void WorkQueue::WorkItem::Run() {
    switch (_type) {
    case wiExit:
        LOGINFO("[%lx] exiting\n", pthread_self());
        pthread_exit(nullptr);
        break;
    case wiSync:
        if (_context) {
            ((Event*)_context)->Wait();
        }
        break;
    case wiRun:
        ((Job*)_context)->Run();
        break;
    default:
        break;
    }
}

void *WorkQueue::StartWorkerthread(void *p) {
    void *ret = nullptr;
    if (p) {
        ret = ((WorkQueue*)p)->Workerthread(p);
    }
    return ret;
}

void *WorkQueue::Workerthread(void *p) {
    while (true) {
        WorkItem *task = nullptr;
        pthread_mutex_lock(&_taskqlock);
        if (!_taskq.empty()) {
            task = _taskq.front();
            _taskq.pop();
        }
        pthread_mutex_unlock(&_taskqlock);
        if (task != nullptr) {
            task->Run();
            delete task;
        }
        else {
            usleep(10000);
        }
    }
}

WorkQueue::WorkQueue(int numthreads) : _numthreads(numthreads), _sync(numthreads) {
    pthread_mutex_init(&_taskqlock, nullptr);
    _threads.reserve(numthreads);
}

WorkQueue::~WorkQueue() {
    if (_threads.size() > 0) {
        JoinAll();
    }
    pthread_mutex_destroy(&_taskqlock);
}

int WorkQueue::CreateThreads() {
    int ret = 0;
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
    for (int i = 0; i<_numthreads ; ++i) {
        pthread_t p = 0;
        ret = pthread_create(&p, &attr, StartWorkerthread, this);
        if (ret == 0) {
            _threads.push_back(p);
        }
        else {
            LOGINFO("pthread_create failed – %08x\n", ret);
            break;
        }
    }
    pthread_attr_destroy(&attr);
    return ret;
}

void WorkQueue::JoinAll() {
    for (auto &t : _threads) {
        pthread_join(t, nullptr);
    }
    _threads.clear();
}

void WorkQueue::AddTask(Job *job) {
    pthread_mutex_lock(&_taskqlock);
    _taskq.push(new WorkItem(wiRun, job));
    pthread_mutex_unlock(&_taskqlock);
}

void WorkQueue::Sync() {
    pthread_mutex_lock(&_taskqlock);
    for (int i = 0 ; i<_numthreads; ++i) {
        _taskq.push(new WorkItem(wiSync, &_sync));
    }
    pthread_mutex_unlock(&_taskqlock);
    _sync.Wait();
}

void WorkQueue::Exit() {
    pthread_mutex_lock(&_taskqlock);
    for (int i = 0 ; i<_numthreads; ++i) {
        _taskq.push(new WorkItem(wiExit, nullptr));
    }
    pthread_mutex_unlock(&_taskqlock);
}

WorkQueue クラスがキューを持っていて、各ワーカー スレッドが放り込まれた WorkItem を逐次処理します。それに加えてある種の同期処理を付け加えました。今回の例で言えば、ばねの力を計算する WorkItem を全部投げ終わった後、速度や位置の計算をするためには、全てのばねの計算が完了していないといけません。つまり、WorkItem が全て処理されたかどうかを WorkQueue 側で判断できる機構が必要になります。

同期処理は、pthread に加えて条件変数 (condition varialbe) を使って実現しています。これは Windows のカーネル オブジェクトの一つであるイベントに似ています。WorkQueue::Sync() を呼ぶと、条件変数のシグナル待ちを引き起こす WorkItem をワーカー スレッドの数だけ投げ込んで、待機状態になったスレッドの数がワーカー スレッドと同じになるまで、Sync() を呼び出しているスレッドもを待機させておく、という動作を行ないます。

話を戻して、ばねの力の計算、すなわち Field::Spring::load() の並列化が失敗した原因は明白で、個々のワークアイテムが細かすぎたことと、ばねの力を加速度のベクトルに代入するときに排他ロックを獲得する必要があるからでしょう。これのせいで、WorkItem を出し入れしているコストがだいぶ高くついてしまいます。もし double をアトミックに加算する命令があれば排他ロックしなくてもいいのですが・・・。アルゴリズムでカバーするとすれば、ばねにつながっているノードがスレッドごとに重複しないように WorkItem を分ける方法がありますが、これだと、全部で C(10000, 2) = 49,995,000ある計算を C(5000, 2) 12,497,500 分は 2 スレッドで処理して残りの 25,000,000 は 1 スレッドで処理する、という感じになって、大して得しなそうなので断念しました。他にもっとうまい分け方を思いついたら試してみます。

速度と位置の計算は、単純に次元毎にスレッドを分けられるため排他ロックの必要はありませんが、やはり WorkItem が細かいので並列化のコストのほうが高くなってしまったのだと考えられます。3 次元プロットだったら使えるのかもしれませんが、まだそこまで手を出す段階じゃない・・。

こんなので土曜日が無駄になってしまった。もったいない。

広告

Use matplotlib in a virtualenv with your own Python build

真っ新の状態の Ubuntu 15.10 に、Python 2.7 をソースからビルドして、matplotlib を virtualenv で動かせるようになるまで一日ぐらいハマったので、コマンドをまとめておきます。

まず、ビルドに必要なパッケージを入れます。

sudo apt-get update
sudo apt-get install build-essential git openssh-server vim

今回インストールするパッケージは以下の通り。2016/1/4 時点での最新バージョンです。

名前 URL バージョン 依存関係
OpenSSL http://www.openssl.org/source/ 1.0.2e なし
zlib http://zlib.net/ 1.2.8 なし
Tcl http://tcl.tk/software/tcltk/download.html 8.6.4 なし
Tk http://tcl.tk/software/tcltk/download.html 8.6.4 Tcl
libx11-dev
Python https://www.python.org/downloads/ 2.7.11 OpenSSL
zlib
Tcl
Tk

Python 以外にソースからビルドが必要になるコンポーネントの理由は↓。もちろん apt-get でインストールすることもできます。

  • numpy/scipy/matplotlib をインストールするときに pip を使う
  • pip は HTTPS で zip をダウンロードしてくるので、OpenSSL と zlib が必要
  • matplotlib でプロットを GUI 表示するために ビルトイン モジュールである _tkinter が必要

まずは OpenSSL。コードは GitHub からクローンします。

$ git clone https://github.com/openssl/openssl.git
$ cd openssl/
$ git checkout refs/tags/OpenSSL_1_0_2e
$ ./config shared –prefix=/usr/local/openssl/openssl-1.0.2e
$ make
$ sudo make install
$ sudo ln -s /usr/local/openssl/openssl-1.0.2e /usr/local/openssl/current

次 zlib。

$ wget http://zlib.net/zlib-1.2.8.tar.gz
$ tar -xvf zlib-1.2.8.tar.gz
$ cd zlib-1.2.8/
$ ./configure –prefix=/usr/local/zlib/zlib-1.2.8
$ make
$ sudo make install
$ sudo ln -s /usr/local/zlib/zlib-1.2.8 /usr/local/zlib/current

次 Tcl。

$ wget http://prdownloads.sourceforge.net/tcl/tcl8.6.4-src.tar.gz
$ tar -xvf tcl8.6.4-src.tar.gz
$ cd tcl8.6.4/unix/
$ ./configure –prefix=/usr/local/tcl/tcl-8.6.4 –enable-shared
$ make
$ sudo make install
$ sudo ln -s /usr/local/tcl/tcl-8.6.4 /usr/local/tcl/current

そして Tk。

$ sudo apt-get install libx11-dev
$ wget
http://prdownloads.sourceforge.net/tcl/tk8.6.4-src.tar.gz
$ tar -xvf tk8.6.4-src.tar.gz
$ cd tk8.6.4/unix/
$ ./configure –prefix=/usr/local/tk/tk-8.6.4 –enable-shared \
–with-tcl=/usr/local/tcl/current/lib
$ make
$ sudo make install
$ sudo ln -s /usr/local/tk/tk-8.6.4 /usr/local/tk/current

ちなみに libx11-dev がインストールされていないと、以下のコンパイル エラーが出ます。

In file included from /usr/src/tk8.6.4/unix/../generic/tkPort.h:21:0,
                 from /usr/src/tk8.6.4/unix/../generic/tkInt.h:19,
                 from /usr/src/tk8.6.4/unix/../generic/tkStubLib.c:14:
/usr/src/tk8.6.4/unix/../generic/tk.h:96:25: fatal error: X11/Xlib.h: No such file or directory
compilation terminated.
Makefile:1164: recipe for target ‘tkStubLib.o’ failed

ここまでは余裕です。次に Python をビルドします。

$ wget https://www.python.org/ftp/python/2.7.11/Python-2.7.11.tgz
$ tar -xvf Python-2.7.11.tgz
$ cd Python-2.7.11/
$ export LDFLAGS=’-L/usr/local/openssl/current/lib -L/usr/local/zlib/current/lib -L/usr/local/tcl/current/lib -L/usr/local/tk/current/lib’
$ export CPPFLAGS=’-I/usr/local/openssl/current/include -I/usr/local/zlib/current/include -I/usr/local/tcl/current/include -I/usr/local/tk/current/include’
$ export LD_LIBRARY_PATH=/usr/local/openssl/current/lib:/usr/local/zlib/current/lib:/usr/local/tcl/current/lib:/usr/local/tk/current/lib
$ ./configure –prefix=/usr/local/python/python-2.7.11 \
–enable-shared –enable-unicode=ucs4
$ make
$ sudo make install
$ sudo ln -s /usr/local/python/python-2.7.11 /usr/local/python/current

make の最後に以下のようなログが出力されるので、必要なビルトイン モジュール (_ssl, _tkinter, zlib) がコンパイルされ、ログに表示されていないことを確認してください。

Python build finished, but the necessary bits to build these modules were not found:
_bsddb             _curses            _curses_panel
_sqlite3           bsddb185           bz2
dbm                dl                 gdbm
imageop            readline           sunaudiodev
To find the necessary bits, look in setup.py in detect_modules() for the module’s name.

失敗例を示します。以下の例では、zlib と _ssl がコンパイルされていないので、後続の手順を実行できません。

Python build finished, but the necessary bits to build these modules were not found:
_bsddb             _curses            _curses_panel
_sqlite3           bsddb185           bz2
dbm                dl                 gdbm
imageop            readline           sunaudiodev
zlib
To find the necessary bits, look in setup.py in detect_modules() for the module’s name.

Failed to build these modules:
_ssl

Python ビルド時のポイントは以下の通りです。

  • ヘッダーやライブラリの有無を自動的に確認し、存在する場合のみモジュールが インストールされる
    (Apache の configure のように、–with-xxx というオプションは存在しない)
  • 予めビルドしておいた zlib や tcl/tk のパスを認識させる必要がある
  • configure 前に環境変数 LDFLAGS, CPPFLAGS を設定しておくと、configure で Makefile に値が反映される
  • ビルトイン モジュールがロードされたときに、依存関係のある共有モジュール tcl/tk を正しく見つからければならないため、環境変数 LD_LIBRARY_PATH で対応
    (後述するが、LD_LIBRARY_PATH ではなく ldconfig でグローバルに追加しておいてもよい。というかその方が楽。)

ここでハマりやすいポイントは共有モジュールの検索パスの追加です。共有モジュールが見つからないと、ビルド後のエクスポート関数のチェックで失敗して、以下のようなエラーが出ます。

// OpenSSL の共有モジュールが見つからない場合
*** WARNING: renaming "_ssl" since importing it failed: build/lib.linux-x86_64-2.7/_ssl.so: undefined symbol: SSLv2_method

// Tcl/Tk の共有モジュールが見つからない場合
*** WARNING: renaming "_tkinter" since importing it failed: build/lib.linux-x86_64-2.7/_tkinter.so: undefined symbol: Tcl_GetCharLength

余談になりますが、./configure –help でパラメーターを確認すると、–with-libs オプションと LIBS 環境変数を使って、追加のライブラリをリンクすることができるように書かれています。今回は共有ライブラリなので、これらのオプションを使ってライブラリを追加する必要がありませんが、実は LIBS 環境変数は罠で、–with-libs オプションを使わないと駄目です。configure を実行すると、–with-libs の値を含めて LIBS 変数を一から作るので、configure 前に LIBS を指定しても上書きされてしまいます。また、ヘルプの書式も分かりにくいのですが、configure は –with-libs の値をそのまま LIBS に追加し、それがリンカのパラメーターの一部になります。したがって、仮に –with-libs を使うときは –with-libs=’-lssl -lz’ というように -l オプションをつけないといけません。実に紛らわしいヘルプです。

$ /usr/src/Python-2.7.11/configure –help
`configure’ configures python 2.7 to adapt to many kinds of systems.

Usage: /usr/src/Python-2.7.11/configure [OPTION]… [VAR=VALUE]…

(snip)

  –with-libs=’lib1 …’  link against additional libs

(snip)

  CFLAGS      C compiler flags
  LDFLAGS     linker flags, e.g. -L<lib dir> if you have libraries in a
              nonstandard directory <lib dir>
  LIBS        libraries to pass to the linker, e.g. -l<library>
  CPPFLAGS    (Objective) C/C++ preprocessor flags, e.g. -I<include dir> if
              you have headers in a nonstandard directory <include dir>
  CPP         C preprocessor

(snip)

これで Python の準備はできました。次に pip をインストールします。方法は幾つかありますが、ここでは get-pip.py を使います。

Installation — pip 7.1.2 documentation
https://pip.pypa.io/en/stable/installing/

ダウンロード時になぜか証明書エラーが出るので –no-check-certificate オプションをつけていますが、基本的には不要です。

$ wget –no-check-certificate https://bootstrap.pypa.io/get-pip.py
$ sudo -H /usr/local/python/current/bin/python get-pip.py

今までの手順通りにやると、ここでエラーが出るはずです。

$ sudo -H /usr/local/python/current/bin/python get-pip.py
Traceback (most recent call last):
  File "get-pip.py", line 17759, in <module>
    main()
  File "get-pip.py", line 162, in main
    bootstrap(tmpdir=tmpdir)
  File "get-pip.py", line 82, in bootstrap
    import pip
  File "/tmp/tmpuPVaWi/pip.zip/pip/__init__.py", line 15, in <module>
  File "/tmp/tmpuPVaWi/pip.zip/pip/vcs/subversion.py", line 9, in <module>
  File "/tmp/tmpuPVaWi/pip.zip/pip/index.py", line 30, in <module>
  File "/tmp/tmpuPVaWi/pip.zip/pip/wheel.py", line 35, in <module>
  File "/tmp/tmpuPVaWi/pip.zip/pip/_vendor/distlib/scripts.py", line 14, in <module>
  File "/tmp/tmpuPVaWi/pip.zip/pip/_vendor/distlib/compat.py", line 31, in <module>
ImportError: cannot import name HTTPSHandler

理由は、sudo で実行している python には LD_LIBRARY_PATH によるライブラリの検索パスが反映されていないためです。sudo -H LD_LIBRARY_PATH=*** /usr/local/python/current/bin/python *** というように一行で実行することもできますが、システム ワイドに検索パスを追加しておいた方が後々楽です。

ここでは /etc/ld.so.conf.d/local.conf というファイルを新しく作って ldconfig を実行します。

john@ubuntu1510:/usr/src$ sudo vi /etc/ld.so.conf.d/local.conf <<< 作成
john@ubuntu1510:/usr/src$ cat /etc/ld.so.conf.d/local.conf
/usr/local/openssl/current/lib
/usr/local/zlib/current/lib
/usr/local/tcl/current/lib
/usr/local/tk/current/lib

john@ubuntu1510:/usr/src$ sudo ldconfig
john@ubuntu1510:/usr/src$ ldconfig -v | grep ^/ <<< 確認
/sbin/ldconfig.real: Path `/lib/x86_64-linux-gnu’ given more than once
/sbin/ldconfig.real: Path `/usr/lib/x86_64-linux-gnu’ given more than once
/usr/lib/x86_64-linux-gnu/libfakeroot:
/usr/local/lib:
/usr/local/openssl/current/lib:
/usr/local/zlib/current/lib:
/usr/local/tcl/current/lib:
/usr/local/tk/current/lib:

/lib/x86_64-linux-gnu:
/sbin/ldconfig.real: /lib/x86_64-linux-gnu/ld-2.21.so is the dynamic linker, ignoring

/usr/lib/x86_64-linux-gnu:
/usr/lib/x86_64-linux-gnu/mesa-egl:
/usr/lib/x86_64-linux-gnu/mesa:
/lib:
/usr/lib:
/sbin/ldconfig.real: Can’t create temporary cache file /etc/ld.so.cache~: Permission denied

これで get-pip.py はうまく実行できるはずです。

$ sudo -H /usr/local/python/current/bin/python get-pip.py
[sudo] password for john:
Collecting pip
  Downloading pip-7.1.2-py2.py3-none-any.whl (1.1MB)
    100% |????????????????????????????????| 1.1MB 204kB/s
Collecting setuptools
  Downloading setuptools-19.2-py2.py3-none-any.whl (463kB)
    100% |????????????????????????????????| 466kB 693kB/s
Collecting wheel
  Downloading wheel-0.26.0-py2.py3-none-any.whl (63kB)
    100% |????????????????????????????????| 65kB 5.4MB/s
Installing collected packages: pip, setuptools, wheel
Successfully installed pip-7.1.2 setuptools-19.2 wheel-0.26.0

pip がインストールされたので、これを使って virtualenv をインストールします。本来であれば、この時点で /usr/local/bin/pip のようなスクリプトが作られて、pip コマンドを実行できるはずなのですか、これまでの手順だとなぜか作られません。それほど支障にはならないので、pip コマンドの代わりに python -m pip <command> という風に python を直に実行します。

$ sudo -H /usr/local/python/current/bin/python -m pip install virtualenv
Collecting virtualenv
  Downloading virtualenv-13.1.2-py2.py3-none-any.whl (1.7MB)
    100% |????????????????????????????????| 1.7MB 333kB/s
Installing collected packages: virtualenv
Successfully installed virtualenv-13.1.2

virtualenv 環境を作ります。

$ /usr/local/python/current/bin/python -m virtualenv ~/Documents/pydev
New python executable in /home/john/Documents/pydev/bin/python
Installing setuptools, pip, wheel…done.
$ cd ~/Documents/pydev/
$ source bin/activate
(pydev)$

virtualenv 内では pip コマンドが使えます。依存パッケージを予めインストールし、普通に pip を使うだけです。

$ sudo apt-get install libblas-dev libatlas-dev liblapack-dev \
gfortran libfreetype6-dev libpng12-dev
$ pip install numpy scipy matplotlib

依存パッケージが存在しなかった時のエラーは以下の通りです。

// scipy インストール時のエラー (1)
  File "scipy/linalg/setup.py", line 20, in configuration
    raise NotFoundError(‘no lapack/blas resources found’)
numpy.distutils.system_info.NotFoundError: no lapack/blas resources found

// scipy インストール時のエラー (2)
building ‘dfftpack’ library
error: library dfftpack has Fortran sources but no Fortran compiler found

// matplotlib インストール時のエラー
* The following required packages can not be built:
* freetype, png

最後に、scatter plot のサンプルを使って動作確認をします。

shapes_and_collections example code: scatter_demo.py — Matplotlib 1.5.0 documentation
http://matplotlib.org/examples/shapes_and_collections/scatter_demo.html

(pydev)$ export TK_LIBRARY=/usr/local/tk/current/lib/tk8.6
(pydev)$ wget
http://matplotlib.org/mpl_examples/shapes_and_collections/scatter_demo.py
(pydev)$ python scatter_demo.py

以下のようなプロットが表示されれば成功です。

scatterdemo

ここで環境変数 TK_LIBRARY を設定しているのは、python からは Tcl が呼ばれているらしく、tk.tcl の検索パスが /usr/local/tcl であり、/usr/local/tk を検索してくれないためです。ファイルは tk 以下にあります。

$ find /usr/local -name tk.tcl
/usr/local/tk/tk-8.6.4/lib/tk8.6/tk.tcl

具体的には、以下のようなエラーが出ます。

  File "/usr/local/python/current/lib/python2.7/lib-tk/Tkinter.py", line 1814, in __init__
    self.tk = _tkinter.create(screenName, baseName, className, interactive, wantobjects, useTk, sync, use)
_tkinter.TclError: Can’t find a usable tk.tcl in the following directories:
    /usr/local/tcl/tcl-8.6.4/lib/tcl8.6/tk8.6 /usr/local/tcl/tcl-8.6.4/lib/tk8.6
/home/john/Documents/pydev/lib/tk8.6 /home/john/Documents/lib/tk8.6 /home/john/Documents/pydev/library

環境変数で対応するのが気持ち悪い場合は、Tcl と Tk を同じディレクトリにインストールしてしまうという手があります。tcl と tk の代わりに、tktcl というディレクトリを作った場合の例を以下にします。こっちのほうがシンプルになってよいかもしれません。

$ wget http://prdownloads.sourceforge.net/tcl/tcl8.6.4-src.tar.gz
$ tar -xvf tcl8.6.4-src.tar.gz
$ cd tcl8.6.4/unix/
$ ./configure –prefix=/usr/local/tktcl/tktcl-8.6.4 –enable-shared
$ make
$ sudo make install
$ sudo ln -s /usr/local/tktcl/tktcl-8.6.4 /usr/local/tktcl/current

$ sudo apt-get install libx11-dev
$ wget
http://prdownloads.sourceforge.net/tcl/tk8.6.4-src.tar.gz
$ tar -xvf tk8.6.4-src.tar.gz
$ cd tk8.6.4/unix/
$ ./configure –prefix=/usr/local/tktcl/tktcl-8.6.4 –enable-shared \
–with-tcl=/usr/local/tktcl/current/lib
$ make
$ sudo make install

$ cat /etc/ld.so.conf.d/local.conf
/usr/local/openssl/current/lib
/usr/local/zlib/current/lib
/usr/local/tktcl/current/lib

$ wget https://www.python.org/ftp/python/2.7.11/Python-2.7.11.tgz
$ tar -xvf Python-2.7.11.tgz
$ cd Python-2.7.11/
$ export LDFLAGS=’-L/usr/local/openssl/current/lib -L/usr/local/zlib/current/lib -L/usr/local/tktcl/current/lib’
$ export CPPFLAGS=’-I/usr/local/openssl/current/include -I/usr/local/zlib/current/include -I/usr/local/tktcl/current/include’
$ ./configure –prefix=/usr/local/python/python-2.7.11 \
–enable-shared –enable-unicode=ucs4
$ make
$ sudo make install
$ sudo ln -s /usr/local/python/python-2.7.11 /usr/local/python/current

今回紹介した matplotlib を virtualenv で使う方法は、ずいぶんとハマっている人が多いです。最初につまずくのは、plt.show() を実行しても何も表示されないという現象です。ほとんどの場合において、Python の _tkinter モジュールが正しくインストールされていないため、matplotlib の backend が non-interactive の agg になっていることが理由と考えられます。これは matplotlib をインポートした後に get_backend() を実行することで確認できます。

// GUI が表示される場合
>>> import matplotlib
>>> matplotlib.get_backend()
u’TkAgg’

// GUI が表示されない場合
>>> import matplotlib
>>> matplotlib.get_backend()
u’agg’

Matplotlib のページを見ると、virtualenv を作るときに –system-site-packages オプションを使うと解決できる、とも書かれていますが、少なくとも今回紹介した手順では、このオプションを使わなくても GUI 表示が可能でした。

Working with Matplotlib in Virtual environments — Matplotlib 1.5.0 documentation
http://matplotlib.org/faq/virtualenv_faq.html