メインコンテンツまでスキップ

GenAIモデルの提供

項目
カテゴリGenAI
難易度初級
推定所要時間15-20 minutes
ラベルgenai, server, llm, vlm, asr, http

ほとんどのアプリケーションでは、GenAIServerとそのOpenAI互換のPOST /v1/chat/completionsエンドポイントから開始します。組み込みのアプリケーションロジックが同じプロセスでモデルの呼び出しを所有する必要がある場合は、直接model.run(request)呼び出しを使用します。

完全なエンドポイントとリクエストコントラクトについては、GenAI Serverリファレンスを参照してください。

ウォークスルー

サーバーの設定

ホストとポートを選択します。デフォルトのホストは0.0.0.0で、これはModalixデバイスにアクセスできる他のマシンからの接続を受け入れます。

tutorials/021_serve_genai_models/serve_genai_models.cpp
genai::GenAIServerOptions options;
options.host = args.host;
options.port = args.port;
genai::GenAIServer server(options);

モデルディレクトリの登録

デプロイされた各モデルディレクトリを、提供する名前とともに追加します。このチュートリアルでは、llmvlm、およびasrを登録します。クライアントがmodelフィールドに送信するのは、この提供する名前です。

tutorials/021_serve_genai_models/serve_genai_models.cpp
if (!args.llm.empty()) {
server.add_model(args.llm, "llm");
}
if (!args.vlm.empty()) {
server.add_model(args.vlm, "vlm");
}
if (!args.asr.empty()) {
server.add_model(args.asr, "asr");
}

std::cout << "registered models:";
for (const auto& name : server.model_names()) {
std::cout << " " << name;
}
std::cout << "\n";

提供の開始

ブロッキングフォアグラウンドプロセスにする場合はserve()を呼び出し、アプリケーションがプロセスの残りのライフサイクルを所有する場合はstart()を呼び出します。

サーバーが開始されたら、GET /v1/modelsを使用して、登録されたモデル名を確認します。

curl http://<modalix-ip>:9998/v1/models

応答には、このチュートリアルで登録された提供する名前、つまりllmvlm、およびasrが含まれている必要があります。

tutorials/021_serve_genai_models/serve_genai_models.cpp
std::cout << "serving on http://" << options.host << ":" << options.port << "\n";
std::cout << "try: curl http://<modalix-ip>:" << options.port << "/v1/models\n";
server.serve();

実行

Modalix DevKitでは、Hugging FaceからLLM、VLM、およびASRモデルをLLiMa CLIを使用してダウンロードします。

llima pull Qwen3-4B-Instruct-2507-GPTQ-a16w4
llima pull Qwen3-VL-4B-Instruct-GPTQ-a16w4
llima pull whisper-small-a16w8

Modalixで、3つのDevKitローカルモデルディレクトリすべてを含むサーバーを開始します。

C++ (prebuilt):

./lib/sima-neat/tutorials/tutorial_021_serve_genai_models \
--llm /media/nvme/llima/models/Qwen3-4B-Instruct-2507-GPTQ-a16w4 \
--vlm /media/nvme/llima/models/Qwen3-VL-4B-Instruct-GPTQ-a16w4 \
--asr /media/nvme/llima/models/whisper-small-a16w8

C++ (build from source):

./build.sh --target tutorial_021_serve_genai_models
./build/tutorials-standalone/tutorial_021_serve_genai_models \
--llm /media/nvme/llima/models/Qwen3-4B-Instruct-2507-GPTQ-a16w4 \
--vlm /media/nvme/llima/models/Qwen3-VL-4B-Instruct-GPTQ-a16w4 \
--asr /media/nvme/llima/models/whisper-small-a16w8

開発中にサブセットのみを提供したい場合は、--vlmまたは--asrを削除します。

サーバーが実行されたら、最初にすべての提供する名前が登録されていることを確認します。

curl http://<modalix-ip>:9998/v1/models

次に、クライアントからエンドポイントを呼び出します。<modalix-ip> を、お使いの Modalix デバイスの IP アドレスまたはホスト名に置き換えてください。 以下に示すリクエストクライアントは、Python の requests を使用し、レスポンスをストリーミングし、サーバー側の TTFT に加えて、報告された場合のトークンごとの平均、最小、および最大 TPS を出力します。

LLM へのテキストリクエスト

python3 share/sima-neat/tutorials/021_serve_genai_models/request_chat_completion_text.py \
--server-ip <modalix-ip> \
--model llm \
"Give me three tips for designing a small REST API."

LLM へのツール呼び出しリクエスト

OpenAI 互換の POST /v1/chat/completions エンドポイントと、Ollama 互換の POST /api/chat エンドポイントは、tools 配列内の関数定義を受け入れます。各エントリには、type: "function"function オブジェクト、および空でない文字列の function.name が必要です。関数の説明と JSON スキーマのパラメータは、function オブジェクト内に含めることができます。

curl http://<modalix-ip>:9998/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "llm",
"messages": [
{"role": "user", "content": "What is the weather in Paris?"}
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
}
}
}
],
"tool_choice": "auto",
"stream": false
}'

tool_choice"auto" に設定すると、モデルが宣言されたツールを選択するか、"none" に設定すると、ツールプロンプトと解析が無効になります。tool_choice を省略するか、null に設定した場合、tools が空でない場合は、"auto" と同じように動作します。不正なツール定義、配列でない tools、およびサポートされていない tool_choice の値または型の場合、HTTP 400 とともに invalid_request_error が返されます。同じツール定義とツール選択の検証は、推論が開始される前に、直接の GenerationRequest 呼び出しにも適用されます。

VLM へのテキストと画像のリクエスト

リクエストスクリプトは、画像を Base64 エンコードし、OpenAI 互換の image_url コンテンツの一部として送信します。

python3 share/sima-neat/tutorials/021_serve_genai_models/request_chat_completion_image.py \
--server-ip <modalix-ip> \
--model vlm \
image.jpg \
"What is the main subject of this image?"

ASR モデルへのオーディオリクエスト

転写クライアントは、デフォルトで自動ソース言語検出を行います。ソース言語がわかっている場合は、--language を使用します。

python3 share/sima-neat/tutorials/021_serve_genai_models/request_audio_transcription.py \
--server-ip <modalix-ip> \
--model asr \
speech.wav

音声を英語に翻訳するには、--translate を追加します。クライアントは、POST /v1/audio/translations に同じ multipart リクエストを送信します。

python3 share/sima-neat/tutorials/021_serve_genai_models/request_audio_transcription.py \
--server-ip <modalix-ip> \
--model asr \
--translate \
speech-in-another-language.wav

転写には、POST /v1/audio/transcriptions を使用します。両方のルートは stream=true をサポートします。提供されたクライアントは、テキストをストリーミングし、検出されたソース言語、no_speech_prob、および最終イベントからの avg_logprob を出力します。no_speech_prob が高いほど、Whisper は入力に音声が含まれていない可能性が高いと判断します。avg_logprob は、生成されたトークンの平均ログ確率であり、値が高い(絶対値が小さい)ほど、より確信を持ってデコードされたことを示します。

実践

ネットワーク境界が役立つ場合にサーバーを使用します。同じプロセス内でオーバーヘッドの少ないアプリケーションコードを実行するために、直接GenAIModelVisionLanguageModel、およびASRModelを呼び出します。

通常のアプリケーションでは、複数のモデル名を登録して、1つのGenAIServerプロセスを実行します。DevKitに十分なメモリがある場合、複数のサーバープロセスは異なるポートにバインドできますが、それぞれが独自のモデルインスタンスをロードし、同じMLAハードウェアゲートキーパーを共有するため、ハードウェアのスループットを増やす方法として扱うべきではありません。

/v1/modelsエンドポイントは、最も簡単な動作確認です。登録されたモデル名が返される場合、サーバーにアクセスでき、モデルレジストリが正しく設定されていることを意味します。

完全なソース

完全なソースプログラムを表示
tutorials/021_serve_genai_models/serve_genai_models.cpp
#include "neat/genai.h"

#include <cstdint>
#include <filesystem>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>

namespace genai = simaai::neat::genai;

struct Args {
std::string host = "0.0.0.0";
std::uint16_t port = 9998;
std::filesystem::path llm;
std::filesystem::path vlm;
std::filesystem::path asr;
};

Args parse_args(int argc, char** argv) {
Args args;
for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i];
if (arg == "--host" && i + 1 < argc) {
args.host = argv[++i];
} else if (arg == "--port" && i + 1 < argc) {
args.port = static_cast<std::uint16_t>(std::stoi(argv[++i]));
} else if (arg == "--llm" && i + 1 < argc) {
args.llm = argv[++i];
} else if (arg == "--vlm" && i + 1 < argc) {
args.vlm = argv[++i];
} else if (arg == "--asr" && i + 1 < argc) {
args.asr = argv[++i];
} else {
throw std::runtime_error("usage: serve_genai_models [--host <host>] [--port <port>] "
"[--llm <dir>] [--vlm <dir>] [--asr <dir>]");
}
}
if (args.llm.empty() && args.vlm.empty() && args.asr.empty()) {
throw std::runtime_error("provide at least one of --llm <dir>, --vlm <dir>, or --asr <dir>");
}
return args;
}

int main(int argc, char** argv) {
try {
const Args args = parse_args(argc, argv);

genai::GenAIServerOptions options;
options.host = args.host;
options.port = args.port;
genai::GenAIServer server(options);

if (!args.llm.empty()) {
server.add_model(args.llm, "llm");
}
if (!args.vlm.empty()) {
server.add_model(args.vlm, "vlm");
}
if (!args.asr.empty()) {
server.add_model(args.asr, "asr");
}

std::cout << "registered models:";
for (const auto& name : server.model_names()) {
std::cout << " " << name;
}
std::cout << "\n";

std::cout << "serving on http://" << options.host << ":" << options.port << "\n";
std::cout << "try: curl http://<modalix-ip>:" << options.port << "/v1/models\n";
server.serve();

return 0;
} catch (const std::exception& e) {
std::cerr << "error: " << e.what() << "\n";
return 1;
}
}

ソース