👌

Intel one API利用時のpybind11

2023/07/03に公開

Intel one API利用時のpybind11

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


2024.08.30
Intel AIkitの仕様が変わったでしょうか?
IntelのPyhtonはanacondaだったようですが、変わっているように見えます。
なんだか挙動が怪しいので、この記述はあまりアテにならなさそうです。


下準備

下準備1~ソースコードの取得

色々な方法はあると思いますが、手っ取り早く、ソースを使う場合です。幸い、pybind11はヘッダオンリライブラリなので、面倒な処理が少なく、ソースから引っ張ってきてもあまり面倒ではないです。というわけで、落としてしまいましょう

Pybind11のgithubへのリンク

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

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

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

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 "Test.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 "Test.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」用に作成します。基本的にはネットに転がっている情報通りで行くと思うのですが、「Python.h」や関係するライブラリをintelのものを指定するようにしないといけないようです。
 2023/7現在、以下のように、CFLASのIオプションと、LIBSのLオプションを指定知れやればOKです。あとは.soファイルを作る普通のコンパイル法になっていると思います。なお、「-I../pybind11-master/include」はpybind11の置き場所に寄りますので自由に変えてOKです。

makefile
# MakeFile

CXX = icpx
CFLAGS = -O3 -std=c++14 -xhost -qopenmp -fPIC -I../pybind11-master/include -I/opt/intel/oneapi/intelpython/latest/include/python3.9
OPTS = -qmkl -ipo  -shared
LIBS = -L`/opt/intel/oneapi/intelpython/latest/bin/python3-config --cflags --ldflags` -L/opt/intel/oneapi/intelpython/latest/lib/
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)

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環境で、pybind11を利用する方法について記載しました。ご参考になれば幸いです。ほかにいい方法がある気もするのでもしご存じなら教えていただければ。

Discussion