カスタムノード開発ガイド¶
概要¶
OpenVINO モデルサーバーのカスタムノードを使用すると、シーケンシャル・モデルの入力と出力が適合しない場合でも、ディープラーニング・モデルを完全なパイプラインにリンクすることができます。多くの場合、あるモデルの出力を別のモデルに直接渡すことはできません。データの分析、フィルター処理、または別の形式への変換が必要になります。この操作は、AI フレームワークでは簡単に実装できないか、サポートされていません。カスタムノードはこの課題に対処します。C++ または C で開発された動的ライブラリーを使用して、任意のデータ変換を実行できます。
カスタムノード API¶
カスタム・ノード・ライブラリーは、custom_node_interface.h で定義された API インターフェイスを実装する必要があります。インターフェイスは、さまざまなコンパイラーとの互換性を簡素化するため C
で定義されています。ライブラリーは、静的または動的にリンクされたサードパーティーのコンポーネントを使用できます。OpenCV は、画像データの操作に使用できる OVMS のビルトイン・コンポーネントです。
API ヘッダーで定義されるデータ構造と関数について説明します。
“CustomNodeTensor” 構造体¶
CustomNodeTensor 構造体は、ノード実行の出力および入力のデータを定義するフィールドで構成されます。カスタムノードは、1 つ以上の他のノードからの複数入力に基づいて結果を生成できます。“Execute” 関数は、処理される複数の入力を格納する複数の CustomNodeTensor オブジェクトへのポインターにアクセスできます。各入力はインデックスを使用して参照することも、名前で検索することもできます。
inputTensor0 = &(inputs[0])
すべての CustomNodeTensor 構造体には次のフィールドが含まれます。
const char* name
- 入力名を表す文字列へのポインター。uint8_t* data
- データバッファーへのポインター。データはバイト単位で保存されます。uint64_t dataBytes
- データ割り当てサイズ (バイト単位)。uint64_t* dims
- 配列の形状サイズを格納するバッファーへのポインター。各次元のサイズは 8 バイトです。uint64_t dimsCount
- データ配列の次元数。CustomNodeTensorPrecision precision
- データ精度の列挙。
CustomNodeTensorInfo 構造体¶
struct CustomNodeTensorInfo のフィールドは CustomNodeTensor に似ています。これは、カスタム・ノード・インターフェイスのメタデータ (入力と出力) 情報を保持します。
“CustomNodeParam” 構造体¶
Struct CustomNodeParam は、パラメーターと値の文字列へのポインターを含むペアのリストを保存します。このオブジェクトの各パラメーターは、構造を反復することでキー名で検索できるインデックスを使用して参照できます。
“execute” 関数¶
int execute(const struct CustomNodeTensor* inputs, int inputsCount, struct CustomNodeTensor** outputs, int* outputsCount, const struct CustomNodeParam* params, int paramsCount);
この関数はカスタムノードのデータ変換を実装します。関数の入力データは、CustomNodeTensor 構造体オブジェクトへのポインター形式で渡されます。これには、すべてのカスタムノード入力のデータとバッファーへのポインターが含まれます。パラメーター inputsCount は、この方法で渡された入力の数に関する情報を渡します。
execute 関数では、入力データを保存するバッファーを変更しないでください。変更すると、他のパイプライン・ノードで使用されるデータが変更される可能性があるためです。
カスタムノード execute 関数の動作は、OVMS 構成で設定されたノード・パラメーターに依存する場合があります。これらは、params
引数で execute 関数に渡されます。paramsCount
は、設定されたパラメーター数の情報を渡します。
データ変換の結果は、ustomNodeTensor 構造体のアドレスを格納するポインターへの出力ポインターによって返される必要があります。出力の数は、関数の実行中に、outputsCount
引数で定義されます。
関数の実行中は、すべての出力データバッファーを割り当てる必要があることに注意してください。これらは、要求の処理が完了し、ユーザーに返された後に OVMS によって解放されます。クリーンアップは、release
関数呼び出しでトリガーされます。この関数もカスタム・ライブラリーに実装する必要があります。
execute
呼び出しによる動的割り当てがパフォーマンスのボトルネックになったり、メモリーの断片化を引き起こしたりすることがあります。2022.1 リリース以降、DAG の初期化中にメモリーを事前に割り当て、後続の推論要求で再利用できるようになりました。以下の initialize
と deinitialize
関数を参照してください。これらは、事前割り当てメモリープールを実装するために使用できます。実装例は、カスタムノードのサンプルソースで確認できます。
Execute 関数は、成功 (0
値) または失敗 (0 以外) を定義する整数値を返します。関数がエラーを報告すると、パイプラインの実行が停止され、ユーザーにエラーが返されます。
“getInputsInfo” 関数¶
この関数は、予期される入力のメタデータに関する情報を返します。返された CustomNodeTensorInfo オブジェクトは、getModelMetadata 呼び出しの応答を作成するために使用されます。ユーザー要求の検証やパイプライン構成の検証にも使用されます。
カスタムノードは、入力データとカスタム・ノード・パラメーターに応じて動的なサイズの結果を生成できます。このような場合、関数 getInputsInfo
は動的サイズの次元で値 0
を返す必要があります。可変解像度またはバッチサイズを入力できます。
“getOutputInfo” 関数¶
前の関数と似ていますが、出力のメタデータを定義します。
“release” 関数¶
この関数は、パイプライン処理の最後に OVMS によって呼び出されます。ノードの実行中に使用されたすべてのメモリー割り当てがクリアされます。execute
関数で出力メモリーを割り当てるために malloc
が使用された場合、この関数は free
を呼び出す必要があります。メモリープールが使用されている場合、この関数は事前に割り当てられたメモリーをプールに返す必要があります。OVMS は、いつ解放するか、どのバッファーを解放するか決定します。
“initialize” 関数¶
int initialize(void** customNodeLibraryInternalManager, const struct CustomNodeParam* params, int paramsCount);
この機能により、予測間で再利用するリソースの作成が可能になります。潜在的な使用例には、最適化された一時バッファー割り当てがあります。initialize
の使用はオプションであり、カスタムノードが機能するのに必須ではありません。customNodeLibraryInternalManager
初期化する場合は、この関数内でインスタンス化する必要があります。初期化が失敗した場合、OVMS がエラーとして扱うように、0
ではないステータスを返す必要があります。
使用しない場合は、最小限のダミー実装が必要です。戻り値 0
はエラーがないことを意味します。
int initialize(void** customNodeLibraryInternalManager, const struct CustomNodeParam* params, int paramsCount) {
return 0;
}
“deinitialize” 関数¶
int deinitialize(void* customNodeLibraryInternalManager);
この機能により、予測の間に使用されたリソースを破棄できます。deinitialize の使用はオプションであり、カスタムノードが機能するのに必須ではありません。deinitialize を使用する場合、customNodeLibraryInternalManager
をここで破棄する必要があります。初期化解除が失敗した場合は、OVMS がエラーとして扱うように、0
以外のステータスを返す必要があります。
使用しない場合は、最小限のダミー実装が必要です。戻り値 0
はエラーがないことを意味します。
int deinitialize(void* customNodeLibraryInternalManager) {
return 0;
}
OpenCV を使用¶
カスタム・ノード・ライブラリーは、静的または動的にリンクできるサードパーティーの依存関係を使用できます。簡素化のため、OVMS Docker イメージに含まれる OpenCV ライブラリーを使用できます。次のようなステートメントを追加するだけです。
#include "opencv2/core.hpp"
文字列のサポート¶
クライアントから文字列として送信された入力をカスタムノードで処理する場合、特別な考慮事項があります。このようなデータは、OVMS フロントエンドで受信されると、形状 [-1,-1] の 2D 配列に自動的に変換されます。この機能を使用するカスタムノードの例には、トークナイザーがあります。
入力¶
2 次元形状と U8 精度 OVMS を持つカスタムノードに文字列が送信されると、そのような入力を含む要求を受信した後、文字列をゼロでパディングされた形状 [文字列の数、最も長い文字列の長さ + 1] の 2 次元 U8 配列に変換します。例えば、3 つの文字列 [“String_123”, “”, “zebra”] のバッチは次のように変換されます。
['S', 't', 'r', 'i', 'n', 'g', '_', '1', '2', '3', 0, // String_123
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // ""
'z', 'e', 'b', 'r', 'a', 0, 0, 0, 0, 0, 0] // "zebra"
出力¶
カスタムノード出力の名前に _string というサフィックスが付いている場合、その形状は 2 次元で、精度は U8 です。OVMS は、そのような出力のデータを、各行にゼロで埋められた文字列を含む配列として扱い、そのような出力のデータを文字列に自動的に変換します。
U8 配列の例:
['S', 't', 'r', 'i', 'n', 'g', '_', '1', '2', '3', 0, // String_123
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // ""
'z', 'e', 'b', 'r', 'a', 0, 0, 0, 0, 0, 0] // "zebra"
上記は [“String_123”, “”, “zebra”] に変換されます。
ビルド¶
カスタム・ノード・ライブラリーは、任意のツールでコンパイルできます。すべてのビルド依存関係が含まれる Docker コンテナに基づいた例に従うことを推奨します。Makefile に記述されています。
テスト¶
カスタム・ライブラリーをテストする推奨方法は、OVMS 実行によるものです。
Makefile で構成された Docker コンテナを使用してライブラリーをコンパイルします。
lib
フォルダーにエクスポートされます。前の手順でコンパイルしたパスのカスタム・ノード・ライブラリーを使用してパイプライン構成を準備します。
OVMS Docker コンテナを起動します。
gRPC または REST クライアントを使用して、OVMS エンドポイントに要求を送信します。
OVMS サーバー上のログを分析します。
デバッグ手順については、OVMS 開発者ガイドを参照してください。
ビルトインのカスタムノード¶
OpenVINO モデルサーバーに加えて、イメージにはビルトインのカスタムノードも提供されています。これらはコンテナ内の /ovms/lib/custom_nodes
ディレクトリーに存在し、構成ファイル内で参照できます。以下に、モデルサーバーの Docker イメージに埋め込まれたカスタムノードのリストを示します。
カスタムノード |
コンテナ内の位置 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
例:
ビルトインの水平 OCR カスタムノードを config.json
に含めると、次のようになります。
...
"custom_node_library_config_list": [
{
"name": "ocr_image_extractor",
"base_path": "/ovms/lib/custom_nodes/libcustom_node_horizontal_ocr.so"
}
],
...
カスタムノードはこのパスですでに使用可能です。何かを構築したりコンテナにマウントする必要はありません。
単体テストには追加の例が含まれています。