GenAI 모델 배포
| 필드 | 값 |
|---|---|
| 범주 | GenAI |
| 난이도 | 초급 |
| 예상 소요 시간 | 15-20 minutes |
| 레이블 | genai, server, llm, vlm, asr, http |
대부분의 애플리케이션의 경우 GenAIServer와 OpenAI 호환 POST /v1/chat/completions 엔드포인트로 시작합니다. 임베디드 애플리케이션 로직이 동일한 프로세스에서 모델 호출을 소유해야 하는 경우 직접 model.run(request) 호출을 사용합니다.
전체 엔드포인트 및 요청 계약은 GenAI 서버 참조를 참조하십시오.
둘러보기
서버 구성
호스트와 포트를 선택합니다. 기본 호스트는 0.0.0.0이며, 이는 Modalix 장치에 연결할 수 있는 다른 시스템의 연결을 허용합니다.
genai::GenAIServerOptions options;
options.host = args.host;
options.port = args.port;
genai::GenAIServer server(options);
모델 디렉터리 등록
배포된 각 모델 디렉터리를 배포된 이름과 함께 추가합니다. 이 튜토리얼에서는 llm, vlm 및 asr을 등록합니다. 배포된 이름은 클라이언트가 model 필드에 보내는 이름입니다.
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
응답에는 이 튜토리얼에서 등록된 배포된 이름인 llm, vlm 및 asr이 포함되어야 합니다.
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에서 세 개의 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로 보냅니다.
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는 생성된 토큰의 평균 로그 확률이며, 값이 높을수록(음수가 작을수록) 디코딩에 대한 신뢰도가 높다는 것을 나타냅니다.
실전 활용
네트워크 경계가 유용할 때 서버를 사용합니다. 동일한 프로세스 내에서 오버헤드가 적은 애플리케이션 코드를 위해 직접 GenAIModel, VisionLanguageModel 및 ASRModel를 호출합니다.
일반적인 애플리케이션의 경우 여러 모델 이름을 사용하여 하나의 GenAIServer 프로세스를 실행합니다. DevKit에 충분한 메모리가 있는 경우 여러 서버 프로세스가 다른 포트에 바인딩될 수 있지만, 각 프로세스는 자체 모델 인스턴스를 로드하고 여전히 동일한 MLA 하드웨어 게이트키퍼를 공유하므로 하드웨어 처리량을 늘리는 방법으로 간주해서는 안 됩니다.
/v1/models 엔드포인트는 가장 빠른 간단한 테스트입니다. 이 엔드포인트에서 제공되는 모델 이름을 반환하면 서버에 연결할 수 있고 모델 레지스트리가 채워진 것입니다.
전체 소스
전체 소스 프로그램 표시
#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;
}
}