🔖

IntelOneAPI + anacondaでのpybind11

2024/08/31に公開

Intel one API利用時のpybind11

C++とPythonを連携させる方法は色々あります。ちょっとしたC++プログラムなら、普通にdllにしてctypesを使えばいいと思いますが、C++オブジェクトと連携させたい場合は面倒です。そういう場合は、個人的には、現状だとpybind11が一番良いのではないかと思っています。ですが、pybind11をIntelコンパイラ環境下で使う場合はどうすればいいのか、というのが情報が少ないようです。

そこで、「c++のコンパイラはoneAPI」「Pythonはanaconda」という環境向けに、pybind11のコンパイル方法を調べてみたので、まとめて報告します。

下準備

下準備1~pybind11の導入

色々な方法はあると思いますが、anaconda環境の場合、pybind11をcondaで入れられます。

conda install pybind11

これだけでOK!

下準備1(別パターン)~ソースコードの取得

オリジナルのソースコードを使う場合です。幸い、pybind11はヘッダオンリライブラリなので、面倒な処理が少なく、ソースから引っ張ってきてもあまり面倒ではないです。落としてきて好きな場所に置いておきましょう。このうちincludeフォルダを使います。

Pybind11のgithubへのリンク

下準備2~oneAPIの必要なキット入手

C++とPythonのIntel one APIの必要なものを入手しましょう。詳しくは公式に。aptでの導入法

必要なツールは以下の2つでしょう。
sudo apt install intel-basekit
sudo apt install intel-hpckit

Pybind11の利用例

今回は、適当にサンプル関数とサンプルクラスを使った例を使ってみます。(下準備1別パターンで落としたPybind11のソースコードは、c++のコードの位置関係は以下と仮定します。)

  • pybind11-master/ (※別パターン利用の場合)
  • test_pybind/
    • TestClass.hpp(クラス定義部分)
    • TestCkass.cpp(クラスメソッドの実装部分)
    • Pybind.cpp(クラスをpybind11で公開する部分)
    • main.py(Pythonスクリプト本体)

C++ソースコード

「TestClass」を定義し、そのメンバを操作するメソッドを一部公開して、Pythonから使えるようにしてみます。C++コード3つは以下のように作成しました。どんなクラスかはコメントに記載しましたが、適当な配列を持ち、それへのゲッタ・セッタ、操作用メソッドを持つ簡単なクラスです。このクラスのメソッド一部をpython側に公開します。

・ヘッダ

TestClass.hpp
#ifndef DEF_TEST_PYBIND
#define DEF_TEST_PYBIND

#include <iostream>
#include <string>

#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>

/* stdコンテナを使う場合はincludeする */
#include <pybind11/stl.h>


namespace py = pybind11;

using namespace std;

/*-------------------*/
/* テスト関数:足し算 */
/*-------------------*/
int add(int x, int y);

/*-------------------*/
/*-------------------*/
/*  テストクラス */
/*-------------------*/
class TestClass {
private:
    /* テストメンバ配列 */
    double* data;
    /* 内部ルーチンテスト用 */
    double temp_rutin();
public:
    /* コンストラクタたち */
    TestClass();
    TestClass(const string& str);
    TestClass(double x1, double x2);
    TestClass(double* x);
    ~TestClass();
    /* ゲッタ例 */
    vector<double> getData();
    double getValue(int i);
    /* セッタ例 */
    void setData(int i, double x);
    void setDataArray(const vector<double>& x);
    /*  テスト関数例*/
    double getSum();
};

#endif

・メソッド実装

TestClass.cpp
#include "TestClass.hpp"
#include <fstream>

/*-------------------*/
/* テスト関数:足し算 */
/*-------------------*/
int add(int x, int y){
    return(x+y);
}

/*-------------------*/
/* コンストラクタ:引数無し */
/*-------------------*/
TestClass::TestClass(){
    cout << "read" << endl;
    fstream fp("./data.dat", std::ios::in);
    double x1, x2;
    fp >> x1 >> x2;
    data = new double[2];
    data[0] = x1;
    data[1] = x2;
}
/*-------------------*/
/* コンストラクタ:ファイル指定文字列 */
/*-------------------*/
TestClass::TestClass(const string& str){
    cout << "read2" << endl;
    fstream fp(str, std::ios::in);
    double x1, x2;
    fp >> x1 >> x2;
    data = new double[2];
    data[0] = x1;
    data[1] = x2;
}
/*-------------------*/
/* コンストラクタ:値指定 */
/*-------------------*/
TestClass::TestClass(double x1, double x2){
    data = new double[2];
    data[0] = x1;
    data[1] = x2;
}
/*-------------------*/
/* コンストラクタ:値指定(配列) */
/*-------------------*/
TestClass::TestClass(double* x){
    data = new double[2];
    data[0] = x[0];
    data[1] = x[1];
}
/*-------------------*/
/* デストラクタ */
/*-------------------*/
TestClass::~TestClass(){
    delete[] data;
    cout << "Del!" << endl;
}

/*-------------------*/
/* セッタ */
/*-------------------*/
void TestClass::setData(int i, double x){
    data[i] = x;
}

/*-------------------*/
/* セッタ */
/*-------------------*/
void TestClass::setDataArray(const vector<double>& x){
    data[0] = x[0];
    data[1] = x[1];
}

/*-------------------*/
/* ゲッタ */
/*-------------------*/
vector<double> TestClass::getData(){
    vector<double> x{data[0], data[1]};
    return x;
}
/*-------------------*/
/* ゲッタ */
/*-------------------*/
double TestClass::getValue(int i){
    return data[i];
}


/*-------------------*/
/* サブルーチン例 */
/*-------------------*/
double TestClass::temp_rutin(){
    return(data[0]+data[1]);
}
/*-------------------*/
/* サブルーチンを使って総和を返す例 */
/*-------------------*/
double TestClass::getSum(){
    double temp = this->temp_rutin();
    return temp;
}

・pybind部分

Pybind.cpp

#include "TestClass.hpp"
#include <fstream>


PYBIND11_MODULE(TestClass, m) {
    /* クラス外部公開・・・使いそうなのを一部だけ公開 */
    py::class_<TestClass>(m, "TestClass")
        .def(py::init())
        .def(py::init<const string&>())
        .def(py::init<double,double>())
        .def("setData", &TestClass::setData)
        .def("setDataArray", &TestClass::setDataArray)
        .def("getData", &TestClass::getData)
        .def("getSum", &TestClass::getSum)
        ;
    /* add関数 */
    m.def("add", &add, "A function which adds two numbers");
}

one API用のmakefile例

上記のcppファイルたちをコンパイルするmakefileを、Intel one APIの「icx/icpx」用に作成します。anaconda環境の場合、基本的にはネットに転がっている情報通りで行くと思います。つまり、関係する「Python.h」などのファイルをanaconda提供のものを指定すればOKです。
 anaconda関係の必要なリンク先は、以下のコマンドを打つと一覧が表示されます。
※(install folder)にはanacondaをインストールしたときのフォルダを入れてください。

/(install folder)/anaconda3/bin/python3-config --cflags --ldflags

すると、2024.8現在、以下のように表示されます。

-I/(install folder)/anaconda3/include/python3.12 -I/(install folder)/anaconda3/include/python3.12 -fno-strict-overflow -march=nocona -mtune=haswell -ftree-vectorize -fPIC -fstack-protector-strong -fno-plt -O3 -ffunction-sections -pipe -isystem /(install folder)/anaconda3/include -fdebug-prefix-map=/croot/python-split_1718722871685/work=/usr/local/src/conda/python-3.12.4 -fdebug-prefix-map=/(install folder)/anaconda3=/usr/local/src/conda-prefix -fuse-linker-plugin -ffat-lto-objects -flto-partition=none -flto -DNDEBUG -O3 -Wall -L/(install folder)/anaconda3/lib/python3.12/config-3.12-x86_64-linux-gnu -L/(install folder)/anaconda3/lib -lpthread -ldl -lutil -lm

この情報を元に、CFLASのIオプションやLオプションでpybind1に必要なinclude先のフォルダを指定してやればOKです。あとは.soファイルを作る普通のコンパイル法になっていると思います。

makefile
# MakeFile

CXX = icpx
CFLAGS = -O3 -std=c++14 -xhost -qopenmp -fPIC -I/(install folder)/anaconda3/include -I/(install folder)/anaconda3/include/python3.12
OPTS = -qmkl -ipo  -shared
LIBS = -L/(install folder)/anaconda3/lib/python3.12/config-3.12-x86_64-linux-gnu -L/(install folder)/anaconda3/lib  -lpthread -ldl  -lutil -lm
OBJDIR = ./obj

SOURCES  = $(wildcard *.cpp)
OBJECTS  = $(addprefix $(OBJDIR)/, $(SOURCES:.cpp=.o))

#target file
TARGET = TestClass.so

$(TARGET): $(OBJECTS) 
	$(CXX) -o $@ $^ $(LDFLAGS) $(LIBS) $(OPTS)
$(OBJDIR)/%.o: %.cpp
	@if [ ! -d $(OBJDIR) ]; \
		then echo "mkdir -p $(OBJDIR) dirs"; mkdir -p $(OBJDIR); \
	fi
	@[ -d $(OBJDIR) ]
	$(CXX) -c $(CFLAGS) $(INDS) -o $@ $<

clean:
	rm $(OBJDIR)/*.o
	rm $(TARGET)
depend:
	makedepend -- -Y -- $(SOURCES)

※-I/(install folder)/anaconda3/include
は、下準備1別パターンを利用した場合は、
-I../pybind11-master/include
です。

Pythonからの実行

makefileで出来た「TestClass.so」をPythonスクリプトと同じフォルダに置けば、importできます。例としては以下のような感じ。

main.py

import TestClass

if __name__ == '__main__':

    #試しにadd関数を読んでみる
    temp2 = TestClass.add(10, 29)
    print("add sample: 10+29 =  ", temp2)
    print("-----\n")

    #C++のクラスのインスタンスを作る(ファイルreadがC++で一回だけ呼ばれる)
    data = TestClass.TestClass("data.dat")

    #クラスのメンバを配列で受け取る
    val = data.getData()
    print("Class Data : ", val)
    print("-----")

    #クラスメンバの0番目に456.0を代入
    data.setData(0, 456.0)
    #再度結果を取得
    val2 = data.getData()
    print("After Class Data : ", val2)
    print("-----")

    #総和を計算してゲットするサンプル
    sum = data.getSum()
    print("sum sample result: ", sum)
    print("-----\n")


    #クラスメンバの0番目に456.0を代入
    data2 = [0.5, 1.0]
    data.setDataArray(data2)
    #再度結果を取得
    val2 = data.getData()
    print("After reset Class Data : ", val2)
    print("-----")

    #総和を計算してゲットするサンプル
    sum = data.getSum()
    print("sum sample result: ", sum)

    #最後にデストラクタが呼ばれる
    print("\n")
#end

実行結果は、以下のようになると思います。

> python ./main.py

add sample: 10+29 =   39
-----

read2
Class Data :  [10.0, 51.2]
-----
After Class Data :  [456.0, 51.2]
-----
sum sample result:  507.2
-----

After reset Class Data :  [0.5, 1.0]
-----
sum sample result:  1.5


Del!

こうすると、Pythonスクリプトでクラスのインスタンスを作ったら、そのインスタンスが不要になるまで、インスタンス内のデータが保持されます。確かに、コンストラクタ実行時に表示される「read2」というのが一回しか呼ばれていません。これは例えば、重たいファイルを読み取ってそのデータを保持し続けたい場合に有利ではないでしょうか(数値解析用のメッシュとか)。
何も考えずに、Pythonの例えばsubprocess.callでC++の実行ファイルを呼ぶ方法だと、上記のような場合は毎回重たいファイルを読み込むので、その時間も結構バカになりませんから。

まとめ

「Intel one API + anaconda 環境」で、pybind11を利用する方法について記載しました。ご参考になれば幸いです。ほかにいい方法がある気もするのでもしご存じなら教えていただければ。

Discussion