본문으로 건너뛰기

직접 API를 사용하여 VLM 실행

필드
범주GenAI
난이도중급
예상 소요 시간10-15 minutes
레이블genai, vlm, image, cache, multimodal

비전-언어 모델은 텍스트와 이미지 텐서를 모두 입력으로 받을 수 있습니다. 하나의 질문에 대해 이미지를 GenerationRequest.images에 직접 첨부합니다. 반복적인 질문의 경우, 이미지를 한 번 인코딩하고 후속 요청에서 캐시된 이미지 임베딩을 재사용합니다.

둘러보기

VLM 및 이미지 로드

배포된 LLiMa 모델 디렉터리에서 VisionLanguageModel을 로드하고 디스크에서 이미지를 디코딩합니다.

OpenCV를 사용하여 이미지를 읽습니다. Neat은 3채널 cv::Mat 입력을 BGR로 처리하고 내부적으로 RGB로 변환합니다.

tutorials/020_run_a_vlm/run_a_vlm.cpp
genai::VisionLanguageModel model(args.model);
cv::Mat image = cv::imread(args.image.string(), cv::IMREAD_COLOR);
if (image.empty()) {
throw std::runtime_error("failed to read image: " + args.image.string());
}

직접 이미지를 사용하여 질문

첫 번째 요청에 이미지를 직접 첨부합니다. 이것은 가장 간단한 방법이며, 단일 시각적 질문에 충분한 경우가 많습니다.

tutorials/020_run_a_vlm/run_a_vlm.cpp
genai::GenerationRequest direct;
direct.prompt = "Describe this image in one sentence.";
direct.images = {image};
direct.max_new_tokens = 96;

const genai::GenerationResult first = model.run(direct);
std::cout << "direct image: " << first.text << "\n\n";

이미지 임베딩 캐시

encode(...)를 호출하여 모델에 이미지 임베딩을 캐시합니다. 호출은 이미지가 수락되고 캐시되었을 때 true를 반환합니다.

tutorials/020_run_a_vlm/run_a_vlm.cpp
if (!model.encode(image)) {
throw std::runtime_error("VLM did not accept the image for caching");
}
std::cout << "cached_images=" << model.cached_image_count() << "\n";

후속 질문

캐시된 이미지를 재사용해야 하는 각 요청에서 use_cached_images = true를 설정합니다. 동일한 캐시된 이미지에 대해 여러 질문을 할 수 있습니다. 해당 플래그가 없는 요청은 정상적으로 작동합니다. 텍스트 전용 요청은 이미지를 사용하지 않고, 직접 이미지 요청은 자체 images를 사용하며, 다른 encode(...) 호출은 캐시된 이미지를 대체합니다.

tutorials/020_run_a_vlm/run_a_vlm.cpp
genai::GenerationRequest cached;
cached.prompt = "What details should I inspect more closely?";
cached.use_cached_images = true;
cached.max_new_tokens = 96;

const genai::GenerationResult follow_up = model.run(cached);
std::cout << "cached image: " << follow_up.text << "\n\n";

genai::GenerationRequest second_cached;
second_cached.prompt = "Summarize the image in three keywords.";
second_cached.use_cached_images = true;
second_cached.max_new_tokens = 48;

const genai::GenerationResult second_follow_up = model.run(second_cached);
std::cout << "cached image keywords: " << second_follow_up.text << "\n\n";

채팅 메시지에 이미지 첨부

messages를 사용하는 경우, 필요한 사용자 메시지에 이미지를 첨부합니다. 이렇게 하면 이미지가 해당 텍스트와 함께 유지됩니다.

tutorials/020_run_a_vlm/run_a_vlm.cpp
genai::ChatMessage image_message;
image_message.role = "user";
image_message.content = "What is the main subject of this image?";
image_message.images = {image};

genai::GenerationRequest message_request;
message_request.messages = {image_message};
message_request.max_new_tokens = 96;

const genai::GenerationResult message_result = model.run(message_request);
std::cout << "message image: " << message_result.text << "\n";

실행

Modalix DevKit에서 LLiMa CLI를 사용하여 Hugging Face에서 LFM2-VL 1.6B VLM을 다운로드합니다.

llima pull LFM2-VL-1.6B-a16w4

Modalix에서 DevKit 로컬 모델 디렉터리와 로컬 이미지를 사용하여 튜토리얼을 실행합니다.

C++ (prebuilt):

./lib/sima-neat/tutorials/tutorial_020_run_a_vlm \
--model /media/nvme/llima/models/LFM2-VL-1.6B-a16w4 \
--image tests/images/people.jpg

C++ (build from source):

./build.sh --target tutorial_020_run_a_vlm
./build/tutorials-standalone/tutorial_020_run_a_vlm \
--model /media/nvme/llima/models/LFM2-VL-1.6B-a16w4 \
--image tests/images/people.jpg

예상되는 출력은 직접 이미지 요청에 대한 하나의 답변, 캐시된 이미지를 재사용하는 여러 개의 후속 답변, 그리고 메시지 수준의 이미지 요청에 대한 하나의 답변입니다.

실전 활용

사용자가 동일한 프레임, 제품 이미지, 다이어그램 또는 문서 페이지에 대해 여러 질문을 할 때 이미지 캐싱을 사용합니다. 각 요청이 다른 이미지를 사용할 때는 캐싱을 피하십시오. 왜냐하면 직접 이미지 경로가 더 간단하고 프롬프트 상태를 명확하게 유지하기 때문입니다.

일부 모델 제품군은 캐시된 재사용을 지원하지 않을 수 있습니다. 이 경우 각 요청에 대해 직접 이미지를 사용하십시오.

대화를 구축하고 단 하나의 메시지만 이미지를 포함해야 할 때 ChatMessage.images를 사용합니다. 더 간단한 단일 프롬프트 형식에는 최상위 GenerationRequest.images를 사용합니다.

전체 소스

전체 소스 프로그램 표시
tutorials/020_run_a_vlm/run_a_vlm.cpp
#include "neat/genai.h"

#include <opencv2/imgcodecs.hpp>

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

namespace genai = simaai::neat::genai;

struct Args {
std::filesystem::path model;
std::filesystem::path image;
};

Args parse_args(int argc, char** argv) {
Args args;
for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i];
if (arg == "--model" && i + 1 < argc) {
args.model = argv[++i];
} else if (arg == "--image" && i + 1 < argc) {
args.image = argv[++i];
} else {
throw std::runtime_error("usage: run_a_vlm --model <vlm_model_dir> --image <image>");
}
}
if (args.model.empty() || args.image.empty()) {
throw std::runtime_error("missing required --model <vlm_model_dir> or --image <image>");
}
return args;
}

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

genai::VisionLanguageModel model(args.model);
cv::Mat image = cv::imread(args.image.string(), cv::IMREAD_COLOR);
if (image.empty()) {
throw std::runtime_error("failed to read image: " + args.image.string());
}

genai::GenerationRequest direct;
direct.prompt = "Describe this image in one sentence.";
direct.images = {image};
direct.max_new_tokens = 96;

const genai::GenerationResult first = model.run(direct);
std::cout << "direct image: " << first.text << "\n\n";

if (!model.encode(image)) {
throw std::runtime_error("VLM did not accept the image for caching");
}
std::cout << "cached_images=" << model.cached_image_count() << "\n";

genai::GenerationRequest cached;
cached.prompt = "What details should I inspect more closely?";
cached.use_cached_images = true;
cached.max_new_tokens = 96;

const genai::GenerationResult follow_up = model.run(cached);
std::cout << "cached image: " << follow_up.text << "\n\n";

genai::GenerationRequest second_cached;
second_cached.prompt = "Summarize the image in three keywords.";
second_cached.use_cached_images = true;
second_cached.max_new_tokens = 48;

const genai::GenerationResult second_follow_up = model.run(second_cached);
std::cout << "cached image keywords: " << second_follow_up.text << "\n\n";

genai::ChatMessage image_message;
image_message.role = "user";
image_message.content = "What is the main subject of this image?";
image_message.images = {image};

genai::GenerationRequest message_request;
message_request.messages = {image_message};
message_request.max_new_tokens = 96;

const genai::GenerationResult message_result = model.run(message_request);
std::cout << "message image: " << message_result.text << "\n";

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

소스