반응형

아래 링크와 같이 WSL에 설치한 vLLM 서버를 실행하고 사용해 보자.

2026.06.19 - [AI, ML, DL] - [vLLM] WSL 에서 vLLM 설치 및 간단한 실행

 

Background에서 서버를 실행하고 프로세스를 확인.

 

■ 서버 실행하기 (Foreground에서 실행된다)

VLLM_USE_FLASHINFER_SAMPLER=0 python3 -m vllhttp://m.entrypoints.openai.api_server --model LGAI-EXAONE/EXAONE-Deep-2.4B-AWQ --trust_remote_code True --gpu_memory_utilization=0.7

 

■ 서버 실행하기 (Background에서 실행되고 vllm.log 파일에 로그가 기록된다)

VLLM_USE_FLASHINFER_SAMPLER=0 python3 -m vllhttp://m.entrypoints.openai.api_server --model LGAI-EXAONE/EXAONE-Deep-2.4B-AWQ --trust_remote_code True --gpu_memory_utilization=0.7 > vllm.log 2>&1 &

 

VLLM_USE_FLASHINFER_SAMPLER=0은 vLLM이 다음 토큰을 선택(샘플링)할 때 FlashInfer 라이브러리의 고속 커널을 사용하지 않고, PyTorch/Triton 기반의 기본 내장 샘플러를 사용하도록 강제하는 환경 변수 설정이다. 내 시스템에선 이 옵션 없이는 실행할 수 없었다.

 

■ 백그라운드에서 동작중인 서버 프로세스 확인 (백그라운드에서 동작하는 프로세스 중 vllm이라는 문자열과 VLLM이라는 문자열을 검색한다)

ps -ef | grep -E "vllm|VLLM"

 

■ 백그라운드 서버 종료

pkill -f vllhttp://m.entrypoints.openai.api_server

 

 

동작중인 서버를 이용해보자. 아래 코드를 작성하고 실행한다.

from openai import OpenAI

# vLLM 서버 주소 설정 (기본 포트 8000)
client = OpenAI(
    base_url="http://localhost:8000/v1",
    api_key="vllm-token" # api_key는 아무 문자열이나 넣어도 된다.
)

# 단발성 답변 받기 (General Request)
print("=== 일반 답변 요청 ===")
response = client.chat.completions.create(
    model="LGAI-EXAONE/EXAONE-Deep-2.4B-AWQ",  # 동작중인 서버의 모델 이름과 일치해야 한다.
    messages=[
        {"role": "system", "content": "당신은 친절한 AI 도우미입니다."},
        {"role": "user", "content": "인공지능과 거대언어모델(LLM)의 차이점을 한 문장으로 설명해줘."}
    ],
    temperature=0.2,
    top_p=0.95,
    max_tokens=1024
)

print(response.choices[0].message.content)

print()

# 실시간 스트리밍 답변 받기 (Streaming Request)
print("=== 스트리밍 답변 요청 ===")
stream = client.chat.completions.create(
    model="LGAI-EXAONE/EXAONE-Deep-2.4B-AWQ",
    messages=[
        {"role": "system", "content": "당신은 친절한 AI 요리사 도우미 입니다."},
        {"role": "user", "content": "맛있는 김치찌개를 끓이는 비법을 짧게 알려줘."}
    ],
    temperature=0.2,
    top_p=0.95,
    max_tokens=1024,
    stream=True, # 스트리밍 활성화
)

for chunk in stream:
    if chunk.choices[0].delta.content is not None:
        print(chunk.choices[0].delta.content, end="", flush=True)

print()

 

답변 첫 부분에 빈 줄과 </thought> 태그가 따라 나온다.

 

불필요한 태그를 제거해 보자.

from openai import OpenAI

# vLLM 서버 주소 설정 (기본 포트 8000)
client = OpenAI(
    base_url="http://localhost:8000/v1",
    api_key="vllm-token" # api_key는 아무 문자열이나 넣어도 된다.
)

# 단발성 답변 받기 (General Request)
print("=== 일반 답변 요청 ===")
response = client.chat.completions.create(
    model="LGAI-EXAONE/EXAONE-Deep-2.4B-AWQ",  # 동작중인 서버의 모델 이름과 일치해야 한다.
    messages=[
        {"role": "system", "content": "당신은 친절한 AI 도우미입니다."},
        {"role": "user", "content": "인공지능과 거대언어모델(LLM)의 차이점을 한 문장으로 설명해줘."}
    ],
    temperature=0.2,
    top_p=0.95,
    max_tokens=1024
)

#print(response.choices[0].message.content) # 태그 포함 답변 출력
content = response.choices[0].message.content

# </thought> 태그가 포함되어 있다면 분할 처리
if "</thought>" in content:
    # thought_process: 모델이 생각한 과정 (필요시 사용)
    # final_answer: 사용자가 원하는 최종 답변
    thought_process, final_answer = content.split("</thought>", 1)
    #print("=== [AI의 생각 과정] ===")
    #print(thought_process.replace("<thought>", "").strip()) # 시작 태그가 남아있다면 제거
    print(final_answer.strip())
else:
    # 태그가 없는 일반적인 경우 그대로 출력
    print(content)

print()

# 실시간 스트리밍 답변 받기 (Streaming Request)
print("=== 스트리밍 답변 요청 ===")
stream = client.chat.completions.create(
    model="LGAI-EXAONE/EXAONE-Deep-2.4B-AWQ",
    messages=[
        {"role": "system", "content": "당신은 친절한 AI 요리사 도우미 입니다."},
        {"role": "user", "content": "맛있는 김치찌개를 끓이는 비법을 짧게 알려줘."}
    ],
    temperature=0.2,
    top_p=0.95,
    max_tokens=1024,
    stream=True, # 스트리밍 활성화
)

# 실시간 문자열에서 불필요한 </thought> 태그 처리를 위한 변수 선언
full_text = ""
has_passed_thought = False

for chunk in stream:
    if chunk.choices[0].delta.content is not None:
        #print(chunk.choices[0].delta.content, end="", flush=True) # 태그 포함 답변 출력

        token = chunk.choices[0].delta.content
        full_text += token  # 들어오는 토큰을 전체 버퍼에 누적

        # </thought> 태그가 지나갔는지 확인
        if not has_passed_thought:
            if "</thought>" in full_text:
                # 태그가 끝나는 지점을 찾아 그 이후의 텍스트만 추출
                _, final_start_text = full_text.split("</thought>", 1)
                has_passed_thought = True
                
                # 태그 뒤에 공백이나 줄바꿈이 있다면 깔끔하게 지우고 첫 출력
                first_output = final_start_text.lstrip()
                if first_output:
                    print(first_output, end="", flush=True)
            else:
                # 아직 </thought> 태그가 나오기 전(생각 중)이라면 화면에 출력하지 않고 건너뛴다.
                continue
        else:
            # 만약 앞서 출력된 내용이 아직 아무것도 없다면 (sys.stdout이 비어있음)
            # 다음에 들어오는 토큰들도 첫 글자가 나올 때까지 앞쪽 공백/줄바꿈을 계속 지워준다.
            if not full_text.split("</thought>", 1)[1].lstrip():
                # 여전히 공백이나 줄바꿈만 들어오는 상태이므로 출력하지 않고 스킵.
                continue
                
            # 글자가 들어오기 시작하면, 그 시점의 토큰부터 그대로 출력.
            # (단, 공백이 섞여 들어왔을 수 있으므로 첫 진입 토큰만 lstrip 처리)
            if len(full_text.split("</thought>", 1)[1].lstrip()) == len(token):
                print(token.lstrip(), end="", flush=True)
            else:
                print(token, end="", flush=True)

print()

 

불필요한 태그 없이 깔끔하게 정리되었다.

 

 

또한 WSL에 서버를 실행한 상태에서 네이티브 윈도우에서도 위와 동일한 코드로 LLM을 사용할 수 있다.

아니면 WSL에서 hostname -I 명령으로 확인한 IP 주소값을 base_url에 넣어도 된다.

from openai import OpenAI

# vLLM 서버 주소 설정 (기본 포트 8000)
client = OpenAI(
    base_url="http://localhost:8000/v1",
    #base_url="http://172.21.167.101:8000/v1",
    api_key="vllm-token" # api_key는 아무 문자열이나 넣어도 된다.
)

# 단발성 답변 받기 (General Request)
print("=== 일반 답변 요청 ===")
response = client.chat.completions.create(
    model="LGAI-EXAONE/EXAONE-Deep-2.4B-AWQ",  # 동작중인 서버의 모델 이름과 일치해야 한다.
    messages=[
        {"role": "system", "content": "당신은 친절한 AI 도우미입니다."},
        {"role": "user", "content": "인공지능과 거대언어모델(LLM)의 차이점을 한 문장으로 설명해줘."}
    ],
    temperature=0.2,
    top_p=0.95,
    max_tokens=1024
)

print(response.choices[0].message.content)

print()

# 실시간 스트리밍 답변 받기 (Streaming Request)
print("=== 스트리밍 답변 요청 ===")
stream = client.chat.completions.create(
    model="LGAI-EXAONE/EXAONE-Deep-2.4B-AWQ",
    messages=[
        {"role": "system", "content": "당신은 친절한 AI 요리사 도우미 입니다."},
        {"role": "user", "content": "맛있는 김치찌개를 끓이는 비법을 짧게 알려줘."}
    ],
    temperature=0.2,
    top_p=0.95,
    max_tokens=1024,
    stream=True, # 스트리밍 활성화
)

for chunk in stream:
    if chunk.choices[0].delta.content is not None:
        print(chunk.choices[0].delta.content, end="", flush=True)

print()

 

네이티브 윈도우에서 WSL 서버 사용.

 

※ 참고

LG AI Research의 공식 가이드에 따르면 EXAONE Deep 모델은 시스템 프롬프트를 지원하지 않거나 권장하지 않는다. 시스템 프롬프트를 넣으면 모델이 지시사항을 무시하거나 추론(<thought>)을 시작하지 못하고 엉뚱한 답변을 낼 확률이 높아진다. 요리사 같은 페르소나는 아래와 같이 user 메시지 안에 녹여내자.

response = client.chat.completions.create(
    model="LGAI-EXAONE/EXAONE-Deep-2.4B-AWQ",
    messages=[
        # 시스템 프롬프트를 없애고 유저 메시지에 역할을 녹여낸다.
        {"role": "user", "content": "당신은 친절한 AI 도우미입니다. 인공지능과 거대언어모델(LLM)의 차이점을 한 문장으로 설명해줘."}
    ],
    temperature=0.2,
    top_p=0.95,
    max_tokens=1024
)

 

반응형
Posted by J-sean
:
반응형

Ollama가 미리 실행되어 있지 않을 때 백엔드를 실행하고 이미지를 분석해 보자.

 

import ollama
import requests
import time
import os
import subprocess

def is_ollama_running(url="http://localhost:11434"):
    try:
        response = requests.get(url)
        return response.status_code == 200
    except requests.ConnectionError:
        return False

def start_ollama():    
    try:        
        if os.name == 'nt': # Windows 환경
            # subprocess.Popen(["ollama", "serve"], shell=True)
            # 이렇게 하면 ollama 백엔드가 실행은되지만 cmd창에 프로그램 결과가 같이 출력되고, cmd창이 ollama 프로세스가 종료될 때까지 닫히지 않음.

            # 새로운 cmd 창을 띄워 ollama 프로세스를 따로 실행하고 창을 유지한다.
            subprocess.Popen(["start", "cmd", "/k", "ollama", "serve"], shell=True)
            #subprocess.Popen("start cmd /k ollama serve", shell=True)
            # cmd /k 는 명령 실행 후에도 종료되지 않고 창을 유지(Keep)시키는 옵션
            
        else: # macOS / Linux 환경
            subprocess.Popen(["ollama", "serve"])
        print("Ollama 백엔드를 실행 중입니다...")
        
        # 서버가 완전히 켜질 때까지 대기
        time.sleep(5)
    except Exception as e:
        print(f"Ollama 실행 중 오류 발생: {e}")

if not is_ollama_running():
    print("Ollama가 실행되어 있지 않습니다. 자동 실행을 시도합니다.")
    start_ollama()
else:
    print("Ollama 백엔드가 이미 실행 중입니다.")

# 모델이 존재하는지 확인하고 없으면 다운로드(Pull)
try:
    models_info = ollama.list()
    #print("Models:\n", models_info)  # 모델 정보 출력 (디버깅용)
    
    # ollama.list() 의 응답 형식에 따라 모델 이름 목록 추출 ('llava' 또는 'llava:latest' 등)
    existing_models = [m['model'] for m in models_info.get('models', [])]
    # [m['model'] for m in models_info.get('models', [])] 는 models_info 딕셔너리에서 'models' 키에
    # 해당하는 리스트를 가져와서, 각 모델(Model) 정보(m)에서 'model' 키의 값을 추출하여 새로운 
    # 리스트(existing_models)를 만드는 코드이다. 이렇게 하면 existing_models 리스트에는 설치된
    # 모델들의 이름이 담기게 된다.
    
    # models_info의 내용:
    # models=[
    #   Model(model='bllossom-korean:latest', modified_at=datetime.datetime(2026, 6, 14, 12, 19, 14, 791297, tzinfo=TzInfo(32400)),
    #       digest='806749e0821a7bdf9ba640baf9f285d5c368246564ca754eaf8508c717910b51', size=2019377929,
    #       details=ModelDetails(parent_model='', format='gguf', family='llama', families=['llama'], parameter_size='3.2B', quantization_level='Q4_K_M')),
    #   Model(model='exaone-deep:latest', modified_at=datetime.datetime(2026, 6, 14, 12, 0, 7, 39358, tzinfo=TzInfo(32400)),
    #       digest='106afe416a9effa9570d04231c25192fc254ac1b51f0f1cafed20a32060958c9', size=4770665152,
    #       details=ModelDetails(parent_model='', format='gguf', family='exaone', families=['exaone'], parameter_size='7.8B', quantization_level='Q4_K_M')),
    #   Model(model='llava:latest', modified_at=datetime.datetime(2026, 6, 14, 10, 58, 59, 368284, tzinfo=TzInfo(32400)),
    #       digest='8dd30f6b0cb19f555f2c7a7ebda861449ea2cc76bf1f44e262931f45fc81d081', size=4733363377,
    #       details=ModelDetails(parent_model='', format='gguf', family='llama', families=['llama', 'clip'], parameter_size='7B', quantization_level='Q4_0'))
    # ]

    #print("Existing Models:", existing_models)  # 설치된 모델 목록 출력 (디버깅용)
    # Existing Models: ['bllossom-korean:latest', 'exaone-deep:latest', 'llava:latest']

    if not any('llava' in m for m in existing_models):        
        answer = input("'llava' 모델이 설치되어 있지 않습니다. 다운로드를 시작하려면 'y'를 입력하세요: ")
        if answer.lower() == 'y':
            ollama.pull('llava')
            print("모델 다운로드 완료!")
        else:
            print("모델 다운로드를 취소했습니다. 프로그램을 종료합니다.")
            exit(0)
    else:
        print("'llava' 모델이 설치되어 있습니다.")

except Exception as e:
    print(f"모델 확인/다운로드 중 오류 발생: {e}")

try:
    response = ollama.chat(
        model='llava',
        messages=[
            {
                'role': 'system',
                'content': '너는 이미지 분석 전문가야. 주어지는 1개의 이미지는 반도체 제조 장비 챔버 내부의 노즐에서 웨이퍼 위에 용액을 뿌리는 상황이야.'
            },
            {
                'role': 'user',
                'content': '이미지를 분석해서 용액이 분사되는 노즐 바디에 약간 흐릿하게 적힌 숫자를 알려주고 전체적인 상황을 설명해줘.',
                'images': ["D:/D/My project/C/suck3.png"]
            }
        ]
    )
    print(f"■ 결과: {response['message']['content']}")

except Exception as e:
    print(f"채팅 중 오류 발생: {e}")

 

 

Ollama 백엔드가 실행되는 창. 종료되지 않고 실행 상태가 계속 유지된다.

 

이미지 분석 결과.

 

코드 변경 없이 다시 실행한 결과. Ollama 백엔드는 위에서 이미 실행되어 있고, 특별히 지시하지 않았지만 이번엔 한글로 결과가 표시되었다.

 

 

불필요한 추측을 하지 못하게 하고 자연스럽지 못한 한국어가 아닌 영어로 질문을 해 보자.

messages=[
    {
        'role': 'system',
        'content': 'You are an image analysis expert.'
    },
    {
        'role': 'user',
        'content': 'Do not provide any explanations. Analyze the image and tell me only the slightly blurred single number on the nozzle body where the solution is being sprayed.',
        'images': ["D:/D/My project/C/suck3.png"]
    }
]

 

정확한 답을 찾았다.

 

messages=[
    {
        'role': 'system',
        'content': 'You are an image analysis expert.'
    },
    {
        'role': 'user',                
        'content': 'Tell me the single number written on the nozzle body where the solution is being sprayed and analyze the image.',
        'images': ["D:/D/My project/C/suck3.png"]
    }
]

 

 

※ 참고

모델이 저장된 폴더가 환경 변수로 등록되어 있어야 한다. 등록되어 있지 않으면 모델을 찾지 못한다.

 

반응형

'AI, ML, DL' 카테고리의 다른 글

[vLLM] WSL에서 vLLM 설치 및 간단한 실행  (1) 2026.06.19
[YOLO] YOLO-World  (0) 2026.06.16
[Ollama] llava 동영상 분석  (0) 2026.06.14
[Ollama] Hugging Face 모델 설치 (Bllossom)  (0) 2026.06.14
[Ollama] Ollama with Python 2  (0) 2026.06.14
Posted by J-sean
:
반응형

파이썬 Ollama 라이브러리를 이용해 llava로 동영상을 분석해 보자.

LLaVA 모델은 기본적으로 이미지 인식(Vision) 모델이기 때문에, MP4 비디오 파일 자체를 한 번에 입력받아 분석할 수는 없다. 대신 OpenCV 같은 라이브러리를 사용하여 비디오에서 특정 프레임(이미지)을 추출한 뒤, 해당 이미지를 LLaVA 모델에 전달하여 상황을 분석하는 방식을 사용해야 한다.

 

import cv2
import ollama

def analyze_video_frame(video_path, frame_number=0):
    # 비디오에서 특정 프레임을 추출하여 LLaVA 모델로 분석합니다.

    print(f"[{video_path}] 비디오를 불러오는 중...")
    
    # 비디오 파일 열기
    cap = cv2.VideoCapture(video_path)
    if not cap.isOpened():
        print("오류: 비디오 파일을 열 수 없습니다. 경로를 확인해주세요.")
        return

    # 원하는 프레임 위치로 이동하여 읽기
    cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
    ret, frame = cap.read()
    cap.release()

    if not ret:
        print(f"오류: {frame_number}번 프레임을 읽을 수 없습니다.")
        return

    print(f"{frame_number}번 프레임 추출 완료. LLaVA 모델에 전송합니다...")

    # OpenCV 이미지(NumPy 배열)를 JPEG 바이트 데이터로 인코딩
    # Ollama API는 바이트 형태의 이미지 데이터를 직접 받을 수 있다.
    success, buffer = cv2.imencode('.jpg', frame)
    # 물리적인 이미지 파일을 생성하지 않는다.
    if not success:
        print("오류: 프레임 인코딩에 실패했습니다.")
        return
        
    frame_bytes = buffer.tobytes()

    # Ollama LLaVA 모델에 프롬프트와 이미지 전달
    #prompt_text = "이 이미지에서 어떤 상황이 벌어지고 있는지 한국어로 자세히 설명해줘."
    prompt_text = "이 이미지에서 어떤 상황이 벌어지고 있는지 자세히 설명해줘."

    try:
        response = ollama.chat(
            model='llava',
            messages=[
                {
                    'role': 'user',
                    'content': prompt_text,
                    'images': [frame_bytes]
                }
            ]
        )
        
        # 결과 출력
        print("\n=== LLaVA 분석 결과 ===")
        print(response['message']['content'])
        print("========================\n")
        
    except Exception as e:
        print(f"Ollama 실행 중 오류 발생: {e}")

if __name__ == "__main__":
    video_file = "D:/D/My project/C/crosswalk_cctv_01.mp4"  # 분석할 비디오 파일 경로
    target_frame = 30          # 분석하고 싶은 프레임 번호 (예: 1초가 30FPS라면 1초 시점)
    
    analyze_video_frame(video_file, target_frame)

 

crosswalk_cctv_01.z01
19.53MB
crosswalk_cctv_01.zip
4.31MB

 

한국어로 설명을 요청하면 자연스럽지 않은 문장을 생성한다.

 

영어 텍스트가 훨씬 자연스럽다.

 

 

하나의 동영상에서 일정한 시간 간격으로 프레임을 추출해 전체적인 흐름을 분석해 보자.

import cv2
import ollama

def analyze_video_flow(video_path, interval_sec=5):
    # 비디오에서 일정 시간(초) 간격으로 프레임을 추출해 전체 흐름을 LLaVA 모델로 분석한다.
    
    print(f"[{video_path}] 비디오 분석 준비 중...")
    
    # 비디오 파일 열기 및 정보 확인
    cap = cv2.VideoCapture(video_path)
    if not cap.isOpened():
        print("오류: 비디오 파일을 열 수 없습니다.")
        return

    fps = cap.get(cv2.CAP_PROP_FPS)
    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    video_length_sec = total_frames / fps
    
    # 5초에 해당하는 프레임 간격 계산
    frame_interval = int(fps * interval_sec)
    
    print(f"비디오 정보: {fps:.2f} FPS / 총 길이: {video_length_sec:.1f}초")
    print(f"{interval_sec}초({frame_interval} 프레임) 간격으로 이미지를 추출합니다...\n")

    frames_bytes = []
    current_frame_pos = 0

    # 5초 간격으로 프레임 순회하며 추출
    while current_frame_pos < total_frames:
        cap.set(cv2.CAP_PROP_POS_FRAMES, current_frame_pos)
        ret, frame = cap.read()
        
        if not ret:
            break
            
        # 콘솔에 진행 상황 표시
        current_time_sec = current_frame_pos / fps
        print(f"{current_time_sec:.1f}초 지점 캡처 완료")

        # 이미지를 바이트 배열로 변환하여 리스트에 추가
        success, buffer = cv2.imencode('.jpg', frame)
        if success:
            frames_bytes.append(buffer.tobytes())

        # 다음 캡처할 프레임 위치로 이동
        current_frame_pos += frame_interval

    cap.release()

    if not frames_bytes:
        print("추출된 프레임이 없습니다.")
        return

    print(f"\n총 {len(frames_bytes)}장의 프레임이 추출되었습니다.")
    print("LLaVA 모델에 데이터 전송 및 분석을 시작합니다. (시간이 조금 걸릴 수 있습니다)...\n")

    # 여러 장의 이미지를 포함하여 LLaVA에 프롬프트 전송
    prompt_text = (
        "첨부된 이미지들은 하나의 비디오에서 5초 간격으로 추출된 프레임들이야."
        "이미지를 하나 하나 설명하지 말고 이 영상에서 어떤 상황이 벌어지고 있는지 전체적인 흐름과 맥락을 요약해서 설명해줘."
    )
    
    try:
        response = ollama.chat(
            model='llava',
            messages=[
                {
                    'role': 'user',
                    'content': prompt_text,
                    'images': frames_bytes  # 여러 장의 이미지가 담긴 리스트를 통째로 전달
                }
            ]
        )
        
        # 최종 결과 출력
        print("=== LLaVA 전체 흐름 분석 결과 ===")
        print(response['message']['content'])
        print("=================================")
        
    except Exception as e:
        print(f"Ollama 실행 중 오류 발생: {e}")

if __name__ == "__main__":
    video_file = "D:/D/My project/C/crosswalk_cctv_01.mp4"  # 테스트할 비디오 파일 경로
    
    # 5초 간격으로 분석 실행
    analyze_video_flow(video_file, interval_sec=5)

 

첫 번째 실행 결과

 

두 번째 실행 결과

 

prompt_text = (
    "첨부된 이미지들은 하나의 비디오에서 5초 간격으로 추출된 프레임들이야."
    "이미지를 하나 하나 설명하지 말고 이 영상에서 어떤 상황이 벌어지고 있는지"
    "전체적인 흐름과 맥락을 요약해서 한글로 설명해줘."
)

한글로 설명을 요청한 결과

 

 

※ 참고

3초 간격으로 프레임을 추출하면 8장의 프레임이 모델로 전달되는데, 사용 가능한 context size를 넘었다는 오류가 발생했다.

 

n_ctx와 n_prompt_tokens는 로컬 LLM을 실행하거나 API 파라미터를 설정할 때 마주치는 핵심 컨텍스트 윈도우(Context Window) 개념입니다
- n_ctx (Context Length): 모델이 한 번에 기억하고 처리할 수 있는 최대 토큰 한도입니다. (예: 4096, 8192, 32768)
- n_prompt_tokens (Prompt Size): 사용자가 입력한 프롬프트(질문, 시스템 지시사항 등)가 차지하는 실제 토큰 크기입니다.
- 동작 원리: 질문이 전송되면 LLM 엔진은 n_ctx 한도 내에서 n_prompt_tokens와 답변 생성에 필요한 토큰을 모두 합산합니다.
만약 질문과 답변(생성 토큰)의 합이 n_ctx 값을 초과하면 에러가 발생하거나 답변이 중간에 잘리게 됩니다.

 

아래와 같이 num_ctx 옵션을 주거나 프레임 추출 간격 늘리기 등의 방법으로 해결 가능하다.

response = ollama.chat(
    model='llava',
    messages=[
        {
            'role': 'user',
            'content': prompt_text,
            'images': frames_bytes  # 여러 장의 이미지가 담긴 리스트를 통째로 전달
        }
    ],
options={
    'num_ctx': 8192  # 기본값보다 크게 설정 (컴퓨터 사양에 따라 16384까지도 가능)
    }
)

 

그런데 이미지 해상도 줄이기로는 해결되지 않는다.

# 해상도를 640x360(또는 더 작게)으로 축소
frame = cv2.resize(frame, (640, 360))
# 이미지를 바이트 배열로 변환하여 리스트에 추가
success, buffer = cv2.imencode('.jpg', frame)
if success:
    frames_bytes.append(buffer.tobytes())

 

간단히 생각하기엔 '해상도가 작으면 용량도 작으니 AI가 처리할 데이터도 줄어들겠지?' 싶지만, LLaVA 같은 비전 언어 모델(VLM)은 그렇게 작동하지 않습니다.

왜 해상도를 줄여도 소용이 없을까?
1. 이미지는 픽셀이 아니라 '토큰(Token)'으로 계산됩니다.
LLaVA 모델 내부에는 이미지를 읽어들이는 '비전 인코더(Vision Encoder)'라는 부품이 있습니다. 이 인코더는 이미지가 들어오면 가로세로를 바둑판처럼 잘게 쪼개서 텍스트와 같은 '토큰'으로 변환합니다.

2. 최소 '기본요금(고정 토큰)'이 존재합니다.
이 비전 인코더는 자신이 처리하기 편한 고정된 내부 해상도(예: 336x336)를 가지고 있습니다.
1920x1080 (FHD) 해상도를 넣으면 => 크기를 줄여서 인식합니다.
100x100 (저해상도) 이미지를 넣으면 => 빈 공간을 채우거나 크기를 늘려서 인식합니다.
결과적으로, 1080p 고화질을 넣든 100x100 썸네일을 넣든 AI 입장에서는 이미지 1장당 약 500~1000개의 고정된 토큰을 무조건 소모해 버립니다.

3. 컨텍스트 윈도우(Context Window)가 꽉 참
Ollama의 기본 기억력(Context Window)은 보통 2048개 또는 4096개 토큰입니다. 해상도를 아무리 줄여도 1장당 700개의 토큰을 차지한다면, 단 4~5장만 보내도 메모리 한도에 도달하게 되는 것입니다.

 

반응형

'AI, ML, DL' 카테고리의 다른 글

[YOLO] YOLO-World  (0) 2026.06.16
[Ollama] Ollama Backend Serve 백엔드 실행  (0) 2026.06.14
[Ollama] Hugging Face 모델 설치 (Bllossom)  (0) 2026.06.14
[Ollama] Ollama with Python 2  (0) 2026.06.14
[Ollama] Ollama with Python 1  (0) 2026.06.13
Posted by J-sean
:
반응형

Hugging Face에서 Bllossom 모델을 다운로드받고 Ollama에서 사용해 보자.

 

llama-3.2-Korean-Bllossom-3B-gguf-Q4_K_M.gguf를 다운로드 받았다.

 

gguf 파일이 위치한 폴더에 Modelfile 파일을 만들고 위와 같이 작성한다.

 

※ 참고

Modelfile Reference

Modelfile

■ 대표적인 주요 파라미터
- temperature: 답변의 창의성을 결정합니다. (0.0 ~ 1.0) 숫자가 높을수록 창의적이고 다양한 답변을, 낮을수록 일관되고 보수적인 답변을 생성합니다. (기본값: 0.8)
- num_ctx: 한 번에 기억하고 처리할 수 있는 최대 토큰(컨텍스트 윈도우) 크기입니다. 값을 크게 할수록 이전 대화를 더 많이 기억하지만, 하드웨어 메모리를 더 많이 사용합니다. (기본값: 2048)
- num_predict: 생성할 답변의 최대 토큰 수입니다. (기본값: -1, 무제한 생성)
- repeat_last_n: 모델이 반복되는 답변을 방지하기 위해 앞의 내용을 얼마나 참고할지 결정합니다. (0 = 끄기, -1 = 컨텍스트 크기만큼) (기본값: 64)
- repeat_penalty: 답변이 반복될 때 이를 얼마나 강하게 페널티를 줄지 설정합니다. (기본값: 1.1)
- top_k: 모델이 다음 토큰을 예측할 때 고려하는 후보군의 수를 제한해 엉뚱한 답변을 줄여줍니다. (기본값: 40)
- top_p: 확률이 높은 토큰을 누적하여 top_p 값에 도달할 때까지 후보군을 추립니다. 값이 낮을수록 더 집중된 답변을 생성합니다. (기본값: 0.9)
- min_p: 가장 확률이 높은 토큰을 기준으로, 그 비율만큼의 확률을 가진 토큰만 후보로 남깁니다. 퀄리티와 다양성을 균형 있게 맞출 때 사용합니다. (기본값: 0.0)
- seed: 난수 생성 시드(Seed)입니다. 동일한 숫자로 지정하면 매번 똑같은 질문에 대해 정확히 똑같은 답변을 얻을 수 있습니다. (기본값: 0)
- stop: 생성을 중단할 특정 문구(Stop sequence)를 지정합니다. 예: stop "User:"

 

ollama create 명령으로 모델을 생성한다.

 

모델을 생성하도록 지정한 폴더에 모델이 생성된다. (위 폴더의 내용이 모델은 아니다)

 

 

반응형

'AI, ML, DL' 카테고리의 다른 글

[Ollama] Ollama Backend Serve 백엔드 실행  (0) 2026.06.14
[Ollama] llava 동영상 분석  (0) 2026.06.14
[Ollama] Ollama with Python 2  (0) 2026.06.14
[Ollama] Ollama with Python 1  (0) 2026.06.13
[Ollama] Ollama 설치 및 간단한 실행  (0) 2026.06.13
Posted by J-sean
:
반응형

파이썬으로 Ollama를 사용해 보자.

 

2026.06.13 - [AI, ML, DL] - [Ollama] Ollama with Python 1

Ollama Python Library

 

pip install ollama

 

간단한 사용법을 확인해 보자.

 

import ollama

response = ollama.generate(model='llava', prompt='한국의 수도는 어디야?')
print(response.response)
#print(response['response'])

 

 

import ollama
import requests

# 인터넷에서 이미지를 다운로드 받아 바이너리 데이터로 준비.
# ollama는 로컬에서 동작하므로 인터넷 URL을 직접 넘겨주면 다운로드하지 못한다.
# requests 패키지 등으로 이미지를 미리 다운로드하여 메모리에 올리거나 파일로 저장한 뒤 전달해야 한다.
image_url = 'https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net' \
'%2Fdna%2FWOyez%2FdJMcacQXuKj%2FAAAAAAAAAAAAAAAAAAAAAFgT1DZJQGvFBLB9ol_9VqLwj5K0O47aH7TEakuYfAZc%2Fimg.jpg' \
'%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1782831599%26allow_ip%3D%26allow_referer' \
'%3D%26signature%3DQqFJijrkmyGM1xuFRwkdn6HxV80%253D'
# 너무 긴 URL이므로 줄바꿈 처리. 실제 코드에서는 한 줄로 작성해도 무방.

response_image = requests.get(image_url)
image_bytes = response_image.content

response = ollama.chat(
	model='llava',
	messages=[
		{
			'role': 'system',
			'content': '너는 이미지 분석 전문가야.'
		},
		{
			'role': 'user',
			'content': '이미지에 무엇이 있는지 한글로 알려줘.',
			'images': [image_bytes]  # 다운로드한 이미지 바이너리 데이터 삽입
		}
	]
)

print(response)
print("■ 생성된 텍스트:", response['message']['content'])

 

 

 

 

생성되는 텍스트를 실시간 스트리밍해 보자.

import ollama
import requests

# 인터넷에서 이미지를 다운로드 받아 바이너리 데이터로 준비.
# ollama는 로컬에서 동작하므로 인터넷 URL을 직접 넘겨주면 다운로드하지 못한다.
# requests 패키지 등으로 이미지를 미리 다운로드하여 메모리에 올리거나 파일로 저장한 뒤 전달해야 한다.
image_url = 'https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net' \
'%2Fdna%2FWOyez%2FdJMcacQXuKj%2FAAAAAAAAAAAAAAAAAAAAAFgT1DZJQGvFBLB9ol_9VqLwj5K0O47aH7TEakuYfAZc%2Fimg.jpg' \
'%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1782831599%26allow_ip%3D%26allow_referer' \
'%3D%26signature%3DQqFJijrkmyGM1xuFRwkdn6HxV80%253D'
# 너무 긴 URL이므로 줄바꿈 처리. 실제 코드에서는 한 줄로 작성해도 무방.

response_image = requests.get(image_url)
image_bytes = response_image.content

response = ollama.chat(
	model='llava',
	messages=[
		{
			'role': 'system',
			'content': '너는 이미지 분석 전문가야.'
		},
		{
			'role': 'user',
			'content': '이미지에 무엇이 있는지 한글로 알려줘.',
			'images': [image_bytes]  # 다운로드한 이미지 바이너리 데이터 삽입
		}		
	],
	stream =True  # 스트리밍 응답을 받도록 설정
)

for chunk in response:
    print(chunk['message']['content'], end='', flush=True)

 

텍스트 생성이 완료된 후 한 번에 출력되지 않고, 순차적으로 생성되는 즉시 연속적으로 스트리밍된다.

 

로컬 이미지 파일을 처리해 보자.

import ollama

response = ollama.chat(
    model='llava',
    messages=[
        {
            'role': 'user',
            'content': '이 이미지를 설명해줘.',
            'images': ['D:/D/My project/C/bus.jpg']
            #'images': ['D:\\D\\My project\\C\\bus.jpg']            
        }
    ]
)
print(response['message']['content'])

 

한글보다 영어 텍스트 생성이 잘 된다.

 

비동기 방식으로 처리해 보자.

import ollama
import asyncio

async def main():	
	response = await ollama.AsyncClient().chat(
		model='llava',
		messages=[
			{
				'role': 'system',
				'content': '너는 이미지 분석 전문가야.'
			},
			{
				'role': 'user',
				'content': '이미지에 무엇이 있는지 한글로 알려줘.',
				'images': ['D:/D/My project/C/bus.jpg']
			}		
		],
		stream=True  # 스트리밍 응답을 받도록 설정
	)

	# 비동기 제너레이터에서 값을 읽기 위해 async for 사용
	async for chunk in response:
		print(chunk['message']['content'], end='', flush=True)

async def run_concurrently():
	# main() 함수를 백그라운드 태스크로 등록하여 실행을 시작함
	task = asyncio.create_task(main())

	# main()이 비동기적으로 실행되는 동안 바로 다음 코드가 실행됨
	for i in range(5):
		print("Another task running...")
		await asyncio.sleep(1)  # 이벤트 루프를 막지 않는 비동기 sleep 사용

	# 백그라운드 태스크(main)가 완료될 때까지 대기
	# 이 줄이 없으면 백그라운드 태스크가 완료되기 전에 프로그램이 종료될 수 있음
	await task

if __name__ == "__main__":	
	# 진입점을 run_concurrently로 변경
	asyncio.run(run_concurrently())

 

더보기

로컬 이미지 파일을 Base64로 인코딩해서 사용할 수도 있다.

import ollama
import base64
import asyncio

async def main():
	# 로컬 이미지 파일을 읽어 Base64로 인코딩
	image_path = r"D:\D\My project\C\bus.jpg"
	with open(image_path, "rb") as image_file:
		base64_image = base64.b64encode(image_file.read()).decode('utf-8')

	response = await ollama.AsyncClient().chat(
		model='llava',
		messages=[
			{
				'role': 'system',
				'content': '너는 이미지 분석 전문가야.'
			},
			{
				'role': 'user',
				'content': '이미지에 무엇이 있는지 한글로 알려줘.',
				'images': [base64_image]  # Base64로 인코딩한 이미지 데이터 삽입
			}		
		],
		stream=True  # 스트리밍 응답을 받도록 설정
	)

	# 비동기 제너레이터에서 값을 읽기 위해 async for 사용
	async for chunk in response:
		print(chunk['message']['content'], end='', flush=True)

async def run_concurrently():
	# main() 함수를 백그라운드 태스크로 등록하여 실행을 시작함
	task = asyncio.create_task(main())

	# main()이 비동기적으로 실행되는 동안 바로 다음 코드가 실행됨
	for i in range(5):
		print("Another task running...")
		await asyncio.sleep(1)  # 이벤트 루프를 막지 않는 비동기 sleep 사용

	# 백그라운드 태스크(main)가 완료될 때까지 대기
	# 이 줄이 없으면 백그라운드 태스크가 완료되기 전에 프로그램이 종료될 수 있음
	await task

if __name__ == "__main__":	
	# 진입점을 run_concurrently로 변경
	asyncio.run(run_concurrently())

 

텍스트 생성과 "Another task running..."이 동시에 진행된다.

 

반응형
Posted by J-sean
:
반응형

파이썬에서 Ollama를 사용해 보자.

 

2026.06.13 - [AI, ML, DL] - [Ollama] Ollama 설치 및 간단한 실행

2026.06.14 - [AI, ML, DL] - [Ollama] Ollama with Python 2

 

import requests
import json

url = "http://localhost:11434/api/generate" # 엔드포인트 URL
# generate는 모델이 텍스트를 생성하는 것을 의미한다.
# 이 엔드포인트는 모델에게 텍스트 생성을 요청하는 역할을 한다.
# 예를 들어, 사용자가 "What is the capital of France?"라는 질문을 보내면, 모델은 이에 대한 답변을 생성하여 반환하게 된다.
payload = {
	"model": "llava",
	"prompt": "What is the capital of France?",
	"stream": False # 스트리밍 여부를 설정하는 옵션으로, False로 설정하면 모델이 생성한 텍스트를 한 번에 반환한다.
			# True로 설정하면 모델이 텍스트를 생성하는 동안 실시간으로 결과를 스트리밍 방식으로 받을 수 있다.
}

headers = { "Content-Type": "application/json" }
response = requests.post(url, data=json.dumps(payload), headers=headers)

if response.status_code == 200:
	print("■ Response:", response.json())
	print()
	print("■ Generated Text:", response.json().get("response", "No generated text found"))
	# response.json().get("response", "No generated text found")는 JSON 응답에서 "response" 키에 해당하는 값을 가져오며,
	# 만약 해당 키가 존재하지 않을 경우 "No generated text found"라는 기본값을 반환한다.
else:
	print("Error:", response.status_code, response.text)

 

한 번에 모든 출력이 완료된다.

 

import requests
import json

url = "http://localhost:11434/api/generate"
payload = {
	"model": "llava",
	"prompt": "What is the capital of France?",
	"stream": True # 스트리밍 여부를 설정하는 옵션으로, False로 설정하면 모델이 생성한 텍스트를 한 번에 반환한다.
					# True로 설정하면 모델이 텍스트를 생성하는 동안 실시간으로 결과를 스트리밍 방식으로 받을 수 있다.
}

headers = { "Content-Type": "application/json" }
response = requests.post(url, data=json.dumps(payload), headers=headers, stream=True)

if response.status_code == 200:
	print("Response:", end=" ")
	# 줄 단위로 데이터를 읽어옴
	for line in response.iter_lines():
		if line:
			# 바이트 데이터를 문자열로 디코딩
			text = line.decode('utf-8')
			result = json.loads(text)
			# 모델이 생성한 텍스트를 출력
			print(result.get("response", ""), end=" ", flush=True)

else:
	print("Error:", response.status_code, response.text)

 

토큰이 생성되는 즉시 실시간으로 출력된다.

 

 

인공지능과 연속적인 대화를 나눠 보자.

import requests
import json

url = "http://localhost:11434/api/chat" # chat은 챗봇과의 대화를 의미하는 것으로, 이 API 엔드포인트는 챗봇과의
# 상호작용을 처리하는 역할을 한다. 클라이언트가 이 URL로 POST 요청을 보내면, 서버는 챗봇 모델을 사용하여 질문에
#  대한 답변을 생성하고 이를 응답으로 반환한다.
payload = {
	"model": "llava",
	"messages": [
		{
			"role": "system",
			"content": "너는 친절한 조수야. 네 이름이 뭐야?"
		},
		{
			"role": "user",
			"content": "한국의 수도는 어디야?"
		}
	],
	"stream": False
}

headers = { "Content-Type": "application/json" }
response = requests.post(url, data=json.dumps(payload), headers=headers)

if response.status_code == 200:
	result = response.json()
	print("답변:", result.get("message", {}).get("content", ""))
	# 챗봇이 생성한 답변은 JSON 응답의 "message" 필드 내의 "content" 필드에 포함되어 있다. 따라서, result.get("message", {}).get("content", "") 코드를 사용하여 답변을 추출하고 출력한다.
	# {}는 get 메서드에서 기본값으로 빈 딕셔너리를 제공하여, "message" 키가 존재하지 않을 경우에도 오류 없이 빈 딕셔너리를 반환하도록 한다. 그리고 다시 get("content", "")를 사용하여 "content" 키가 존재하지 않을 경우 빈 문자열을 반환하도록 한다.
else:
	print("Error:", response.status_code, response.text)

 

 

 

 

이미지를 분석해 보자.

import requests
import json
import base64

# Ollama의 llava와 같은 멀티모달(시각-언어) 모델에 이미지를 전달하려면, 이미지 파일의 경로를 프롬프트에 텍스트로 적는 것이
# 아니라 이미지 파일을 읽어서 Base64 방식으로 인코딩한 뒤, 페이로드의 images 배열 매개변수로 전달해야 한다.

# 로컬 이미지 파일을 읽어 Base64로 인코딩
image_path = r"D:\D\My project\C\bus.jpg"
with open(image_path, "rb") as image_file:
	base64_image = base64.b64encode(image_file.read()).decode('utf-8')

url = "http://localhost:11434/api/generate" # 엔드포인트 URL
# generate는 모델이 텍스트를 생성하는 것을 의미한다.
# 이 엔드포인트는 모델에게 텍스트 생성을 요청하는 역할을 한다.
# 예를 들어, 사용자가 "What is the capital of France?"라는 질문을 보내면, 모델은 이에 대한 답변을 생성하여 반환하게 된다.
payload = {
	"model": "llava",
	"prompt": "이 사진은 어디서 찍었을까? 한글로 대답해줘.",
	"stream": False, # 스트리밍 여부를 설정하는 옵션으로, False로 설정하면 모델이 생성한 텍스트를 한 번에 반환한다.
	"images": [base64_image] # Base64 문자열로 인코딩된 이미지를 리스트 형태로 전달
}

headers = { "Content-Type": "application/json" }
response = requests.post(url, data=json.dumps(payload), headers=headers)

if response.status_code == 200:	
	print("Response:", response.json().get("response", "No generated text found"))
	# response.json().get("response", "No generated text found")는 JSON 응답에서 "response" 키에 해당하는 값을 가져오며,
	# 만약 해당 키가 존재하지 않을 경우 "No generated text found"라는 기본값을 반환한다.
else:
	print("Error:", response.status_code, response.text)

 

 

 

반응형
Posted by J-sean
:
반응형

Ollama를 설치하고 사용해 보자.

Documentation

API Reference

 

Windows 버전을 설치한다.

 

 

특별한 옵션도 없고 그냥 설치하면 된다.

 

help

 

아직 설치된 모델이 없다.

 

 

llava 모델을 설치하고 사용해 보자. LLava 모델은 한글 사용이 가능하지만 자연스럽지는 않다.

Large Language and Vision Assistant

 

ollama pull llava

 

 

ollama run llava를 실행하면 llava 모델이 실행된다.

 

D:\D\My project\C\bus.jpg

 

 

질문을 입력하면 답변이 출력된다.

 

답변은 영어로 나오지만 한글 질문도 알아듣는다.

 

이미지 경로를 다시 입력하지 않아도 정확히 인식한다.

 

llava 모델의 경우 자연스럽지는 않지만 한글 출력도 가능하다.

 

모델 삭제

 

※ 참고

vLLM

vLLM Documentation

vLLM GitHub

 

반응형
Posted by J-sean
: