OpenVINO GenAI로 추론하기

OpenVINO GenAI로 추론하기

OpenVINO GenAI는 기본 OpenVINO 런타임을 확장해 생성형 AI 모델을 더 효율적으로 다루게 해주는 파이프라인·메서드 라이브러리예요. 텍스트 생성은 물론이고 텍스트-이미지, 음성 인식, 텍스트-음성, 비전 언어 모델까지 폭넓은 유스케이스를 몇 줄의 코드로 실행할 수 있죠. 다만 아래 안내는 GenAI가 포함된 OpenVINO 설치를 전제로 하니, 기본 OpenVINO만 설치돼 있다면 먼저 OpenVINO GenAI 패키지를 설치하고 시작해야 해요. 이 글에서는 각 시나리오별 참고 코드와 실제 사용 요령을 함께 다뤄볼게요.

출처: Inference with OpenVINO GenAI — OpenVINO™ documentation

텍스트-이미지 생성 (Text-to-Image)

OpenVINO GenAI의 openvino_genai.Text2ImagePipeline은 Stable Diffusion 1.5, 2.1, XL, LCM, Flex 같은 텍스트-이미지 모델 추론을 담당해요. 사용 예시를 Python과 C++로 각각 보여드릴게요.

Python에서 Text2ImagePipeline을 만들고 generate에 프롬프트와 이미지 크기, 추론 스텝 수를 넘기는 기본 형태예요.

import argparse

import openvino_genai
from PIL import Image


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('model_dir')
    parser.add_argument('prompt')
    args = parser.parse_args()

    device = 'CPU'  # GPU can be used as well
    pipe = openvino_genai.Text2ImagePipeline(args.model_dir, device)

    image_tensor = pipe.generate(
        args.prompt,
        width=512,
        height=512,
        num_inference_steps=20,
        num_images_per_prompt=1)

    image = Image.fromarray(image_tensor.data[0])
    image.save("image.bmp")

여러 개의 LoRA 어댑터를 동시에 적용할 수도 있어요. 어댑터와 그에 따른 알파 값을 명령줄 파라미터에서 읽어 AdapterConfig에 담아 두는 방식이죠. 생성자에 넘긴 어댑터는 기본적으로 이후 모든 generate 호출에 적용돼요.

import openvino as ov
import openvino_genai

def image_write(path: str, image_tensor: ov.Tensor):
    from PIL import Image
    image = Image.fromarray(image_tensor.data[0])
    image.save(path)


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('models_path')
    parser.add_argument('prompt')
    args, adapters = parser.parse_known_args()

    prompt = args.prompt

    device = "CPU"  # GPU, NPU can be used as well
    adapter_config = openvino_genai.AdapterConfig()

    # Multiple LoRA adapters applied simultaneously are supported, parse them all and corresponding alphas from cmd parameters:
    for i in range(int(len(adapters) / 2)):
        adapter = openvino_genai.Adapter(adapters[2 * i])
        alpha = float(adapters[2 * i + 1])
        adapter_config.add(adapter, alpha)

    # LoRA adapters passed to the constructor will be activated by default in next generates
    pipe = openvino_genai.Text2ImagePipeline(args.models_path, device, adapters=adapter_config)

    print("Generating image with LoRA adapters applied, resulting image will be in lora.bmp")
    image = pipe.generate(prompt,
                          width=512,
                          height=896,
                          num_inference_steps=20,
                          rng_seed=42)

    image_write("lora.bmp", image)
    print("Generating image without LoRA adapters applied, resulting image will be in baseline.bmp")
    image = pipe.generate(prompt,
                          # passing adapters in generate overrides adapters set in the constructor; openvino_genai.AdapterConfig() means no adapters
                          adapters=openvino_genai.AdapterConfig(),
                          width=512,
                          height=896,
                          num_inference_steps=20,
                          rng_seed=42)
    image_write("baseline.bmp", image)

C++에서는 ov::genai::Text2ImagePipeline을 쓰면 동일한 동작을 할 수 있어요. width, height, num_inference_steps 같은 파라미터는 ov::genai:: 네임스페이스의 래퍼 함수로 넘기죠.

#include "openvino/genai/image_generation/text2image_pipeline.hpp"

#include "imwrite.hpp"

int32_t main(int32_t argc, char* argv[]) try {
    OPENVINO_ASSERT(argc == 3, "Usage: ", argv[0], " <MODEL_DIR> '<PROMPT>'");

    const std::string models_path = argv[1], prompt = argv[2];
    const std::string device = "CPU";  // GPU can be used as well

    ov::genai::Text2ImagePipeline pipe(models_path, device);
    ov::Tensor image = pipe.generate(prompt,
        ov::genai::width(512),
        ov::genai::height(512),
        ov::genai::num_inference_steps(20),
        ov::genai::num_images_per_prompt(1));

    // writes `num_images_per_prompt` images by pattern name
    imwrite("image_%d.bmp", image, true);

    return EXIT_SUCCESS;
} catch (const std::exception& error) {
    try {
        std::cerr << error.what() << '\n';
    } catch (const std::ios_base::failure&) {}
    return EXIT_FAILURE;
} catch (...) {
    try {
        std::cerr << "Non-exception object thrown\n";
    } catch (const std::ios_base::failure&) {}
    return EXIT_FAILURE;
}

LoRA를 쓰는 C++ 예시도 동일한 원리예요. ov::genai::AdapterConfig에 어댑터와 알파를 쌓고, generate에서 어댑터를 비워(ov::genai::adapters()) 기본 모델만 쓰는 비교 이미지를 만들 수도 있어요.

#include "openvino/genai/image_generation/text2image_pipeline.hpp"

#include "imwrite.hpp"

int32_t main(int32_t argc, char* argv[]) try {
    OPENVINO_ASSERT(argc >= 3 && (argc - 3) % 2 == 0, "Usage: ", argv[0], " <MODEL_DIR> '<PROMPT>' [<LORA_SAFETENSORS> <ALPHA> ...]]");

    const std::string models_path = argv[1], prompt = argv[2];
    const std::string device = "CPU";  // GPU, NPU can be used as well

    ov::genai::AdapterConfig adapter_config;
    // Multiple LoRA adapters applied simultaneously are supported, parse them all and corresponding alphas from cmd parameters:
    for(size_t i = 0; i < (argc - 3)/2; ++i) {
        ov::genai::Adapter adapter(argv[3 + 2*i]);
        float alpha = std::atof(argv[3 + 2*i + 1]);
        adapter_config.add(adapter, alpha);
    }

    // LoRA adapters passed to the constructor will be activated by default in next generates
    ov::genai::Text2ImagePipeline pipe(models_path, device, ov::genai::adapters(adapter_config));

    std::cout << "Generating image with LoRA adapters applied, resulting image will be in lora.bmp\n";
    ov::Tensor image = pipe.generate(prompt,
        ov::genai::width(512),
        ov::genai::height(896),
        ov::genai::num_inference_steps(20),
        ov::genai::rng_seed(42));
    imwrite("lora.bmp", image, true);

    std::cout << "Generating image without LoRA adapters applied, resulting image will be in baseline.bmp\n";
    image = pipe.generate(prompt,
        ov::genai::adapters(),  // passing adapters in generate overrides adapters set in the constructor; adapters() means no adapters
        ov::genai::width(512),
        ov::genai::height(896),
        ov::genai::num_inference_steps(20),
        ov::genai::rng_seed(42));
    imwrite("baseline.bmp", image, true);

    return EXIT_SUCCESS;
} catch (const std::exception& error) {
    try {
        std::cerr << error.what() << '\n';
    } catch (const std::ios_base::failure&) {}
    return EXIT_FAILURE;
} catch (...) {
    try {
        std::cerr << "Non-exception object thrown\n";
    } catch (const std::ios_base::failure&) {}
    return EXIT_FAILURE;
}

음성 인식 (Speech Recognition)

Whisper 계열 모델 추론은 WhisperPipeline 클래스로 처리해요. 입력은 16kHz 샘플링 레이트의 WAV 형식 오디오예요. Python에서 librosa로 WAV를 읽어 파이프라인에 넘기고, generatelanguage, task, return_timestamps 같은 파라미터를 지정하면 돼요.

import openvino_genai
import librosa


def read_wav(filepath):
    raw_speech, samplerate = librosa.load(filepath, sr=16000)
    return raw_speech.tolist()


def infer(model_dir: str, wav_file_path: str):
    device = "CPU"  # GPU or NPU can be used as well.
    pipe = openvino_genai.WhisperPipeline(model_dir, device)

    # The pipeline expects normalized audio with a sampling rate of 16kHz.
    raw_speech = read_wav(wav_file_path)
    result = pipe.generate(
        raw_speech,
        max_new_tokens=100,
        language="<|en|>",
        task="transcribe",
        return_timestamps=True,
    )

    print(result)

    for chunk in result.chunks:
        print(f"timestamps: [{chunk.start_ts}, {chunk.end_ts}] text: {chunk.text}")

C++에서는 WhisperGenerationConfigmax_new_tokens, language, task, return_timestamps를 미리 세팅한 뒤 generate에 넘겨요. result.chunks를 순회하면 각 구간의 타임스탬프와 텍스트를 얻을 수 있죠.

#include "audio_utils.hpp"
#include "openvino/genai/whisper_pipeline.hpp"

int main(int argc, char* argv[]) try {
    if (3 > argc) {
        throw std::runtime_error(std::string{"Usage: "} + argv[0] + " <MODEL_DIR> \"<WAV_FILE_PATH>\"");
    }

    std::filesystem::path models_path = argv[1];
    std::string wav_file_path = argv[2];
    std::string device = "CPU";  // GPU or NPU can be used as well.

    ov::genai::WhisperPipeline pipeline(models_path, device);

    ov::genai::WhisperGenerationConfig config(models_path / "generation_config.json");
    config.max_new_tokens = 100;
    config.language = "<|en|>";
    config.task = "transcribe";
    config.return_timestamps = true;

    // The pipeline expects normalized audio with a sampling rate of 16kHz.
    ov::genai::RawSpeechInput raw_speech = utils::audio::read_wav(wav_file_path);
    auto result = pipeline.generate(raw_speech, config);

    std::cout << result << "\n";

    for (auto& chunk : *result.chunks) {
        std::cout << "timestamps: [" << chunk.start_ts << ", " << chunk.end_ts << "] text: " << chunk.text << "\n";
    }

} catch (const std::exception& error) {
    try {
        std::cerr << error.what() << '\n';
    } catch (const std::ios_base::failure&) {
    }
    return EXIT_FAILURE;
} catch (...) {
    try {
        std::cerr << "Non-exception object thrown\n";
    } catch (const std::ios_base::failure&) {
    }
    return EXIT_FAILURE;
}

텍스트-음성 생성 (Text-to-Speech)

openvino_genai.Text2SpeechPipeline은 SpeechT5 TTS 같은 텍스트-음성 모델을 위한 파이프라인이에요. 화자의 음색 특성을 제어하려면 스피커 임베딩 벡터를 지정하면 되고, 임베딩을 안 넘기면 모델 내장 스피커를 기본으로 써요. 스피커 임베딩은 저장소의 create_speaker_embedding.py 스크립트로 만들 수 있어요.

import numpy as np
import openvino as ov
import openvino_genai
import soundfile as sf

device = "CPU"
speaker_embedding = np.fromfile("speaker_embedding.bin", dtype=np.float32).reshape(1, 512)
speaker_embedding = ov.Tensor(speaker_embedding)

pipe = openvino_genai.Text2SpeechPipeline(model_dir, device)

result = pipe.generate(args.text, speaker_embedding)

speech = result.speeches[0]
sf.write("output_audio.wav", speech.data[0], samplerate=16000)

채팅 시나리오에서 GenAI 사용하기

입출력이 대화 형태인 채팅 시나리오에서는 입력 간에 KVCache를 유지하는 게 도움이 돼요. start_chatfinish_chat 메서드로 대화 세션을 표시하면 되는데, 대화 중간중간 프롬프트를 넣고 generate로 답변을 받는 구조예요. 아래는 Python 예시예요.

import openvino_genai


def streamer(subword):
    print(subword, end='', flush=True)
    return False


def infer(model_dir: str):
    device = 'CPU'  # GPU can be used as well.
    pipe = openvino_genai.LLMPipeline(model_dir, device)

    config = openvino_genai.GenerationConfig()
    config.max_new_tokens = 100

    pipe.start_chat()
    while True:
        try:
            prompt = input('question:\n')
        except EOFError:
            break
        pipe.generate(prompt, config, streamer)
        print('\n----------')
    pipe.finish_chat()

C++에서도 pipe.start_chat()std::getline(std::cin, prompt)로 질문을 받아 generatepipe.finish_chat() 흐름이 동일해요. streamer 람다는 토큰을 출력하고, 생성이 끝났는지를 나타내는 bool을 반환하죠.

#include "openvino/genai/llm_pipeline.hpp"

int main(int argc, char* argv[]) try {
    if (2 != argc) {
        throw std::runtime_error(std::string{"Usage: "} + argv[0] + " <MODEL_DIR>");
    }
    std::string prompt;
    std::string models_path = argv[1];

    std::string device = "CPU";  // GPU, NPU can be used as well
    ov::genai::LLMPipeline pipe(models_path, device);

    ov::genai::GenerationConfig config;
    config.max_new_tokens = 100;
    std::function<bool(std::string)> streamer = [](std::string word) {
        std::cout << word << std::flush;
        return false;
    };

    pipe.start_chat();
    std::cout << "question:\n";
    while (std::getline(std::cin, prompt)) {
        pipe.generate(prompt, config, streamer);
        std::cout << "\n----------\n"
            "question:\n";
    }
    pipe.finish_chat();
} catch (const std::exception& error) {
    try {
        std::cerr << error.what() << '\n';
    } catch (const std::ios_base::failure&) {}
    return EXIT_FAILURE;
} catch (...) {
    try {
        std::cerr << "Non-exception object thrown\n";
    } catch (const std::ios_base::failure&) {}
    return EXIT_FAILURE;
}

JavaScript/TypeScript(Node.js)에서도 LLMPipeline을 쓸 수 있어요. startChat으로 세션을 열고 readline으로 질문을 받아 pipe.generate(prompt, config)로 답변을 얻은 뒤, 종료 시 finishChat()을 호출하는 흐름이에요.

import { LLMPipeline } from "openvino-genai-node";
import readline from 'readline';

const pipe = await LLMPipeline(model_path, 'CPU');

const config = { 
    max_new_tokens: 100, 
    num_beam_groups: 3, 
    num_beams: 15, 
    diversity_penalty: 1.5 
};

await pipe.startChat();

const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
});

console.log('question:');
rl.on('line', async (prompt) => {
    console.log('answer:');
    const answer = await pipe.generate(prompt, config);
    console.log(answer);
    console.log('\n----------\nquestion:');
});

rl.on('close', async () => {
    // highlight-next-line
    await pipe.finishChat();
    process.exit(0);
});

비전 언어 모델(VLM)과 GenAI

openvino_genai.VLMPipeline은 LLava, MiniCPM-V 같은 멀티모달 텍스트 생성 모델(VLM) 추론을 담당해요. 텍스트 프롬프트와 이미지를 입력으로 받아 텍스트를 생성하죠. Python에서는 이미지를 Tensor로 읽어 pipe.generate(prompt, images=rgbs, ...) 형태로 넘겨요.

import numpy as np
import openvino_genai
from PIL import Image
from openvino import Tensor
from pathlib import Path


def streamer(subword: str) -> bool:
    print(subword, end='', flush=True)


def read_image(path: str) -> Tensor:
    pic = Image.open(path).convert("RGB")
    image_data = np.array(pic.getdata()).reshape(1, pic.size[1], pic.size[0], 3).astype(np.uint8)
    return Tensor(image_data)


def read_images(path: str) -> list[Tensor]:
    entry = Path(path)
    if entry.is_dir():
        return [read_image(str(file)) for file in sorted(entry.iterdir())]
    return [read_image(path)]


def infer(model_dir: str, image_dir: str):
    rgbs = read_images(image_dir)
    device = 'CPU'  # GPU can be used as well.
    enable_compile_cache = dict()
    if "GPU" == device:
        enable_compile_cache["CACHE_DIR"] = "vlm_cache"
    pipe = openvino_genai.VLMPipeline(model_dir, device, **enable_compile_cache)

    config = openvino_genai.GenerationConfig()
    config.max_new_tokens = 100

    pipe.start_chat()
    prompt = input('question:\n')
    pipe.generate(prompt, images=rgbs, generation_config=config, streamer=streamer)

    while True:
        try:
            prompt = input("\n----------\n"
                "question:\n")
        except EOFError:
            break
        pipe.generate(prompt, generation_config=config, streamer=streamer)
    pipe.finish_chat()

채팅봇 유스케이스 — 단계별로 만들기

ov_genai.LLMPipeline과 대화에 맞춰 튜닝된 TinyLlama 모델로 채팅봇을 만드는 과정을 보여드릴게요. 아래 샘플은 추론 장치로 CPU를 쓰지만 GPU로 바꿔도 돼요. 참고로 토큰 선택·토크나이제이션·디토크나이제이션 같은 작업은 항상 CPU에서만 처리되고, 별도 모델로 표현되는 토크나이저도 CPU에서 실행돼요.

모델 실행하기

먼저 Hugging Face의 Optimum-Intel로 LLM 모델을 내보내요. 원래 fp16 대신 int4 정밀도를 쓰면 성능이 좋아지는데, 가중치 압축은 모델 내보내기 단계에서 NNCF가 처리해요. 내보낸 모델에는 토크나이저/디토크나이저와 생성 설정까지 실행에 필요한 정보가 모두 들어 있어 Hugging Face에서 생성한 결과와 일치해요.

주의: meta-llama/Llama-2-7b-chat-hf 모델을 쓰려면 라이선스 동의가 필요해요. Hugging Face Hub에 가입하고 모델 카드의 이용 약관을 읽고 동의한 뒤, 아래 코드 실행에 필요한 액세스 토큰을 사용해야 해요.

LLMPipeline이 텍스트 생성을 위한 핵심 객체예요. 변환된 모델과 추론 장치, 추가 파라미터를 여기에 넘겨요.

optimum-cli export openvino --model "TinyLlama/TinyLlama-1.1B-Chat-v1.0" --weight-format int4 --trust-remote-code "TinyLlama-1.1B-Chat-v1.0"
import openvino_genai as ov_genai
pipe = ov_genai.LLMPipeline(model_path, "CPU")
print(pipe.generate("The Sun is yellow because", max_new_tokens=100))
#include "openvino/genai/llm_pipeline.hpp"
#include <iostream>

int main(int argc, char* argv[]) {
   std::string model_path = argv[1];
   ov::genai::LLMPipeline pipe(model_path, "CPU");
   std::cout << pipe.generate("The Sun is yellow because", ov::genai::max_new_tokens(100));
}

출력 스트리밍하기

생성 중에 더 인터랙티브한 UI를 원한다면 출력 토큰을 스트리밍할 수 있어요. 아래는 람다가 생성 직후 토큰을 콘솔에 출력하도록 만든 예시예요.

import openvino_genai as ov_genai
pipe = ov_genai.LLMPipeline(model_path, "CPU")

streamer = lambda x: print(x, end='', flush=True)
pipe.generate("The Sun is yellow because", streamer=streamer, max_new_tokens=100)
#include "openvino/genai/llm_pipeline.hpp"
#include <iostream>

int main(int argc, char* argv[]) {
   std::string model_path = argv[1];
   ov::genai::LLMPipeline pipe(model_path, "CPU");

   auto streamer = [](std::string word) {
      std::cout << word << std::flush;
      // Return flag indicating whether generation should be stopped.
      // false means continue generation.
      return false;
   };
   pipe.generate("The Sun is yellow because", ov::genai::streamer(streamer), ov::genai::max_new_tokens(100));
}

더 정교한 처리가 필요하면 커스텀 스트리머를 만들어 쓸 수도 있어요. Python에서는 StreamerBase를 상속받아 put(token_id)end()를 구현하죠. put의 반환값이 생성 중단 여부를 나타내는데, 반환을 생략하면 None으로 처리되어 False(계속 생성)로 해석돼요.

import openvino_genai as ov_genai

class CustomStreamer(ov_genai.StreamerBase):
   def __init__(self, tokenizer):
      ov_genai.StreamerBase.__init__(self)
      self.tokenizer = tokenizer
   def put(self, token_id) -> bool:
      # Decode tokens and process them.
      # Streamer returns a flag indicating whether generation should be stopped.
      # In Python, `return` can be omitted. In that case, the function will return None
      # which will be converted to False, meaning that generation should continue.
      # return stop_flag
   def end(self):
      # Decode tokens and process them.

pipe = ov_genai.LLMPipeline(model_path, "CPU")
pipe.generate("The Sun is yellow because", streamer=CustomStreamer(), max_new_tokens=100)

그룹 빔 서치로 생성 최적화하기

더 좋은 텍스트 생성 품질과 효율적인 배치 처리를 원한다면 generation_config로 그룹 빔 서치 디코딩을 지정할 수 있어요.

import openvino_genai as ov_genai
pipe = ov_genai.LLMPipeline(model_path, "CPU")
config = pipe.get_generation_config()
config.max_new_tokens = 256
config.num_beam_groups = 3
config.num_beams = 15
config.diversity_penalty = 1.0
pipe.generate("The Sun is yellow because", config)
int main(int argc, char* argv[]) {
   std::string model_path = argv[1];
   ov::genai::LLMPipeline pipe(model_path, "CPU");
   ov::genai::GenerationConfig config = pipe.get_generation_config();
   config.max_new_tokens = 256;
   config.num_beam_groups = 3;
   config.num_beams = 15;
   config.diversity_penalty = 1.0f;

   cout << pipe.generate("The Sun is yellow because", config);
}

추측 디코딩으로 생성 속도 올리기

추측 디코딩(또는 assisted-generation)은 메인 모델 옆에 더 작은 드래프트 모델을 함께 써서 토큰 생성을 빠르게 해주는 기법이에요. 드래프트 모델이 다음 K개의 토큰을 자회귀적으로 예측하면, 메인 모델이 그 예측을 검증하고 어긋난 부분만 자기 예측으로 교정해요. 그러면 메인 모델에 대한 추론 요청 수가 줄어 성능이 올라가죠.

import openvino_genai
import queue
import threading

def streamer(subword):
        print(subword, end='', flush=True)
        return False

def infer(model_dir: str, draft_model_dir: str, prompt: str):
    main_device = 'CPU'  # GPU can be used as well.
    draft_device = 'CPU'

    scheduler_config = openvino_genai.SchedulerConfig()
    scheduler_config.cache_size = 2

    draft_model = openvino_genai.draft_model(draft_model_dir, draft_device)

    pipe = openvino_genai.LLMPipeline(model_dir, main_device, scheduler_config=scheduler_config, draft_model=draft_model)

    config = openvino_genai.GenerationConfig()
    config.max_new_tokens = 100
    config.num_assistant_tokens = 5

    pipe.generate("The Sun is yellow because", config, streamer)

C++에서도 ov::genai::draft_model(...)ov::genai::scheduler_config(...)로 동일하게 구성하면 돼요. num_assistant_tokens은 드래프트 모델이 한 번에 몇 개 토큰을 제안할지를 정하는 값이에요.

#include <openvino/openvino.hpp>

#include "openvino/genai/llm_pipeline.hpp"

int main(int argc, char* argv[]) try {
    if (4 != argc) {
        throw std::runtime_error(std::string{"Usage: "} + argv[0] + " <MODEL_DIR> <DRAFT_MODEL_DIR> '<PROMPT>'");
    }

    ov::genai::GenerationConfig config;
    config.max_new_tokens = 100;
    config.num_assistant_tokens = 5;

    std::string main_model_path = argv[1];
    std::string draft_model_path = argv[2];
    std::string prompt = argv[3];

    std::string main_device = "CPU", draft_device = "CPU";

    ov::genai::SchedulerConfig scheduler_config;
    scheduler_config.cache_size = 5;

    ov::genai::LLMPipeline pipe(
        main_model_path,
        main_device,
        ov::genai::draft_model(draft_model_path, draft_device),
        ov::genai::scheduler_config(scheduler_config));

    auto streamer = [](std::string subword) {
        std::cout << subword << std::flush;
        return false;
    };

    pipe.generate("The Sun is yellow because", config, streamer);
} catch (const std::exception& error) {
    try {
        std::cerr << error.what() << '\n';
    } catch (const std::ios_base::failure&) {}
    return EXIT_FAILURE;
} catch (...) {
    try {
        std::cerr << "Non-exception object thrown\n";
    } catch (const std::ios_base::failure&) {}
    return EXIT_FAILURE;
}

GGUF 모델 추론

Hugging Face에 배포된 일부 언어 모델은 GGUF(GGML Unified Format)로 제공돼요. GGUF 모델은 메타데이터와 모델 가중치를 포함한 전체 LLM 파이프라인을 이진 파일 하나에 담고 있어, 지원되는 아키텍처라면 추가 변환 없이 OpenVINO GenAI에서 바로 쓸 수 있어요. optimum-intel로 IR(Intermediate Representation)로 변환할 필요가 없다는 뜻이죠.

이 기능은 현재 프리뷰 모드로, SmolLM, Qwen2.5 같은 일부 토폴로지만 지원해요. 그 외 모델은 여전히 optimum-intel로 IR 형식 변환을 권장해요.

import openvino_genai

pipe = openvino_genai.LLMPipeline("SmolLM2-135M.F16.gguf", "CPU")

config = openvino_genai.GenerationConfig()
config.max_new_tokens = 100

pipe.generate("The Sun is yellow because", config)
#include <openvino/openvino.hpp>
#include "openvino/genai/llm_pipeline.hpp"

int main(int argc, char* argv[]) try {
    ov::genai::GenerationConfig config;
    config.max_new_tokens = 100;

    std::string model_path = "SmolLM2-135M.F16.gguf";
    std::string prompt = "The Sun is yellow because";

    ov::genai::LLMPipeline pipe(model_path, "CPU");

    auto result = pipe.generate("The Sun is yellow because", config);
    std::cout << "result = " << result << std::endl;
} catch (const std::exception& error) {
    try {
        std::cerr << error.what() << '\n';
    } catch (const std::ios_base::failure&) {}
    return EXIT_FAILURE;
} catch (...) {
    try {
        std::cerr << "Non-exception object thrown\n";
    } catch (const std::ios_base::failure&) {}
    return EXIT_FAILURE;
}

Hugging Face 결과와 비교하기

같은 프롬프트로 OpenVINO와 Hugging Face가 각각 생성한 결과를 비교할 수도 있어요. 아래 코드는 do_sample=False로 동일한 최대 토큰 수를 생성해 둘의 출력이 일치하는지 assert로 확인해요.

from transformers import AutoTokenizer, AutoModelForCausalLM
import openvino_genai as ov_genai

tokenizer = AutoTokenizer.from_pretrained("TinyLlama/TinyLlama-1.1B-Chat-v1.0")
model = AutoModelForCausalLM.from_pretrained("TinyLlama/TinyLlama-1.1B-Chat-v1.0")

max_new_tokens = 32
prompt = 'table is made of'

encoded_prompt = tokenizer.encode(prompt, return_tensors='pt', add_special_tokens=False)
hf_encoded_output = model.generate(encoded_prompt, max_new_tokens=max_new_tokens, do_sample=False)
hf_output = tokenizer.decode(hf_encoded_output[0, encoded_prompt.shape[1]:])
print(f'hf_output: {hf_output}')

pipe = ov_genai.LLMPipeline('TinyLlama-1.1B-Chat-v1.0')
ov_output = pipe.generate(prompt, max_new_tokens=max_new_tokens)
print(f'ov_output: {ov_output}')

assert hf_output == ov_output

GenAI API

이 글에서 다룬 유스케이스는 다음 OpenVINO GenAI API 클래스를 중심으로 구성돼요.

  • generation_config — 텍스트 생성 설정 클래스. 생성 텍스트의 최대 길이, 문장 끝 토큰 무시 여부, 디코딩 전략(그리디·빔 서치·멀티노미얼 샘플링)을 커스터마이즈해요.
  • llm_pipeline — 입력 처리, 텍스트 생성, 옵션을 적용한 출력 관리용 클래스와 유틸리티를 제공해요.
  • streamer_base — 스트리머를 만들기 위한 추상 베이스 클래스예요.
  • tokenizer — 텍스트를 인코딩·디코딩하는 토크나이저 클래스예요.

더 알아보기 (Learn more)