학습 자료

모델을 얹는다 — 그래픽카드를 아예 안 쓰고


Article

여기까지 모델을 한 번도 안 불렀다. 데이터를 넣고, 벡터로 바꾸고, 가까운 상품을 찾는 데까지 왔다.

지금 어디까지 왔나
  1. DB1·2편 · 끝
  2. 벡터3·4·5편 · 끝
  3. 후보 찾기5편 · 끝
  4. 말을 붙인다이번 편
앞의 셋은 모델 없이 됐다. 마지막 한 겹에만 모델이 필요하다.

1. 무엇이 말썽이었나

모델을 내 컴퓨터에서 돌리는 도구는 그래픽카드에 모델을 얹으려고 한다. 빠르기 때문이다. 그런데 여기서 노트북마다 갈린다.

설치는 다 되는데 여기서 갈린다
  1. 파이썬 코드다 똑같다
  2. 올라마 설치여기까지는 전원 통과
  3. 그래픽카드드라이버가 제각각
  4. 모델 얹기여기서 실패한다
코드가 틀린 게 아니다. 드라이버 버전과 그래픽 메모리 크기가 노트북마다 달라서 나는 일이다.

그래서 아예 안 쓴다. 느려지지만, 느린 것은 기다리면 되고 안 되는 것은 기다려도 안 된다.

2. 올라마를 확인한다

올라마(Ollama)는 모델을 내 컴퓨터에서 돌려주는 프로그램이다. ollama.com 에서 받아 설치한다.

설치가 끝나면 터미널에서 확인한다.

ollama --version
터미널
  • ollama version is 0.32.6

어떤 모델이 있는지 본다

ollama list
터미널
  • NAME ID SIZE MODIFIED
  • qwen2.5:3b 357c53fb659c 1.9 GB 2 days ago

없으면 받는다. 2GB 쯤 되니 시간이 걸린다.

ollama pull qwen2.5:3b
모델 이름 뜯어보기
qwen2.5
모델 이름과 판 번호
알리바바가 공개한 것
3b
값이 30억 개
b = billion(십억)
크기
1.9GB
값 하나를 반 바이트로 눌러 담았다

3. 그래픽카드를 떼어낸 판을 만든다

받아둔 모델을 그대로 쓰지 않고, 그래픽카드에 올릴 겹 수를 0 으로 못 박은 판을 하나 더 만든다.

작업 폴더에 Modelfile 이라는 파일을 만든다. 확장자가 없다.

Modelfile
FROM qwen2.5:3b
PARAMETER num_gpu 0
두 줄이 각각 하는 일
줄무슨 뜻인가
FROM qwen2.5:3b받아둔 이 모델을 바탕으로 삼는다. 새로 받지 않는다
PARAMETER num_gpu 0그래픽카드에 올릴 겹 수 = 0. 전부 CPU 로 계산한다
모델은 여러 겹으로 되어 있고, 올라마가 「몇 겹까지 그래픽카드에 올릴지」를 스스로 정한다. 그 값을 0으로 고정하는 것이다.
ollama create qwen-cpu -f Modelfile
터미널
  • writing manifest
  • success

-f Modelfile 은 「이 파일에 적힌 대로 만들어라」 는 뜻이다. f 는 file 이다.

진짜로 안 쓰는지 확인한다

만들었다는 말만 믿지 않는다. 한 번 돌려놓고 어디에서 계산 중인지 본다.

ollama run qwen-cpu "안녕"
ollama ps
터미널
  • NAME ID SIZE PROCESSOR CONTEXT
  • qwen-cpu:latest c04b4cf67c53 2.2 GB 100% CPU 4096
같은 모델, 다른 판

qwen2.5:3b

받아둔 그대로

  • 그래픽카드에 올린다
  • 빠르다 — 초당 99토큰
  • 노트북마다 다르게 실패한다

qwen-cpu

지금 만든 것

  • PROCESSOR 100% CPU
  • 느리다 — 초당 20~25토큰
  • 어디서든 똑같이 돈다
네 배쯤 느리다. 그런데 안 돌아가는 것을 붙잡고 있는 시간을 생각하면 이쪽이 훨씬 싸다.

4. 파이썬에서 부른다

python -m pip install openai

접속 설정을 파일 하나로

앞으로 만들 파일이 여럿이다. 접속 설정을 그 파일들에 흩뿌리면 안 된다.

config.py
"""누구에게 물어볼지를 여기서 한 번만 정한다.

다른 파일은 전부 이렇게 쓴다 ─
    from config import client, MODEL
"""

from openai import OpenAI

# 어디로 보낼지. localhost = 내 컴퓨터 자신, 11434 = 올라마가 쓰는 문 번호.
# 끝의 /v1 은 "표준 방식으로 말을 걸겠다"는 표시다. 빠뜨리면 404 가 난다
BASE_URL = "http://localhost:11434/v1"

# 원래는 열쇠를 넣는 자리인데, 내 컴퓨터는 검사를 안 한다.
# 그래도 비워두면 패키지가 에러를 내므로 아무 글자나 채운다
API_KEY = "ollama"

# 방금 만든 판의 이름
MODEL = "qwen-cpu"

# 이 한 줄로 "말을 걸 상대"가 만들어진다. 다른 파일들은 이걸 가져다 쓴다
client = OpenAI(base_url=BASE_URL, api_key=API_KEY)
세 값이 각각 하는 일
BASE_URL
어디로 보낼지
localhost:11434/v1
API_KEY
누가 보냈는지
여기선 안 쓴다
MODEL
누가 답할지
qwen-cpu

5. 첫 호출

11_hello.py
# config.py 에서 client 와 MODEL 을 가져온다.
# 접속 설정이 저기 한 군데에만 있으니 여기엔 주소도 열쇠도 안 적힌다
from config import client, MODEL

res = client.chat.completions.create(
    model=MODEL,

    # messages 는 주고받은 말의 목록이다. 지금은 우리가 한 말 하나뿐이다.
    #   role="user"       내가 하는 말
    #   role="assistant"  모델이 한 말 (이어서 물을 때 넣는다)
    #   role="system"     "너는 이런 역할이다" 라는 지시 (뒤에서 쓴다)
    messages=[
        {"role": "user", "content": "화장품 추천 서비스를 한 문장으로 소개해줘"},
        {"role": "system", "content": "한국어로 답변해줘" },
    ],
)

# 답이 여러 개 올 수도 있는 구조라 목록이다. 보통 하나뿐이라 [0] 으로 꺼낸다.
# choices[0].message.content 가 실제 답 글자다
print(res.choices[0].message.content)

# usage 에는 이번 호출에 쓴 토큰 수가 들어 있다.
# 내 컴퓨터에서 돌리면 돈은 안 들지만, 이 숫자만큼 시간이 든다
print("입력", res.usage.prompt_tokens, "출력", res.usage.completion_tokens)
터미널
  • 화장품 추천 서비스는 고객이 특정 피부 유형이나 피부 문제에 맞는 가장 적합한 제품을 찾아내주는 도우미입니다.
  • 입력 43 출력 36
낯선 이름 풀이
이름무슨 뜻인가왜 여기 나오나
from config import ...다른 파일에서 가져오기접속 설정을 한 군데에만 두려고
messages주고받은 말의 목록대화 형태로 보내는 방식이다
role누가 한 말인지user 나 · assistant 모델 · system 지시
choices[0]답 후보 중 첫 번째보통 하나만 온다
.message.content실제 답 글자이걸 화면에 쓴다
usage쓴 토큰 수시간과 돈의 척도

6. 얼마나 느린지 재둔다

느린 것은 참을 수 있지만 얼마나 느린지 모르는 것은 못 참는다. 앞으로 쓸 실제 크기로 재본다.

12_measure.py
import time                       # 시간을 재는 도구. 파이썬에 기본으로 들어 있다

from config import client, MODEL

prompt = """[고객] 41세 여성 건성
[이력] 병풀 링클 클렌징오일 28100(별점3) / 히알루론산 탄력 클렌징오일 25000(별점4)
[후보]
P115 비타민C 트러블 클렌징오일 20400
P140 히알루론산 시카 클렌징오일 23200
P096 레티놀 브라이트닝 클렌징오일 24400

어울릴 상품 두 개를 골라라."""

for i in range(3):                      # 세 번 돌린다. 한 번만 재면 못 믿는다
    started = time.time()                   # 지금 시각 (1970년부터 흐른 초)

    res = client.chat.completions.create(
        model=MODEL,
        messages=[{"role": "user", "content": prompt}],
    )

    took = time.time() - started            # 끝난 시각 - 시작 시각 = 걸린 초

    n_out = res.usage.completion_tokens
    print(f"{i + 1}회  {took:5.1f}초 · 출력 {n_out:3d}토큰 · 초당 {n_out / took:.1f}토큰")
터미널
  • 1회 13.2초 · 출력 264토큰 · 초당 20.0토큰
  • 2회 17.0초 · 출력 421토큰 · 초당 24.7토큰
  • 3회 10.3초 · 출력 255토큰 · 초당 24.7토큰

7. 안 될 때

이 순서로 확인한다
  1. 01

    ① 올라마가 떠 있나

    브라우저에서 http://localhost:11434 를 연다. 「Ollama is running」이 나와야 한다. 안 나오면 트레이(시계 옆) 아이콘을 확인한다.

  2. 02

    ② 모델이 있나

    ollama list 에 qwen-cpu 가 보이는지 본다. 없으면 ollama create 를 다시 한다.

  3. 03

    ③ 진짜 CPU 인가

    ollama run qwen-cpu "안녕" 뒤에 ollama ps 를 본다. 100% CPU 가 아니면 Modelfile 의 num_gpu 0 을 확인하고 다른 이름으로 다시 만든다.

  4. 04

    ④ 404 가 난다

    config.py 의 주소 끝에 /v1 이 빠졌다. 제일 많이 나는 실수다.

  5. 05

    ⑤ 답이 아예 안 온다

    메모리가 모자라 못 얹는 경우다. 다른 프로그램을 닫고 다시 해본다. 그래도 안 되면 오래 붙잡지 말고 다음 편(상용 API)으로 간다 — config.py 세 줄만 바꾸면 된다.

부록 — ollama 명령이 안 먹을 때

명령어를 못 쓰는 상황이면 파이썬으로 같은 일을 할 수 있다. 올라마는 프로그램이 말을 거는 창구도 함께 열어두기 때문이다.

부록_make_cpu_model.py
# urllib 은 파이썬에 기본으로 들어 있는 인터넷 도구다 (URL 을 다루는 도구 묶음).
# 설치할 것이 없어서 어떤 컴퓨터에서든 그냥 돈다
import json
import urllib.request

# Request 는 "보낼 요청서"를 만든다. 아직 보내지는 않는다
req = urllib.request.Request(
    "http://localhost:11434/api/create",       # 11434 = 올라마의 문 번호

    # 보낼 내용. ollama create 에 준 것과 같은 내용을 형식만 바꾼 것이다.
    # json.dumps 는 파이썬 값을 글자로 바꾸고,
    # .encode() 는 그 글자를 바이트로 바꾼다 — 컴퓨터끼리는 바이트로 오간다
    data=json.dumps({
        "model": "qwen-cpu",                   # 만들 이름
        "from": "qwen2.5:3b",                  # 바탕이 될 모델
        "parameters": {"num_gpu": 0},          # Modelfile 의 PARAMETER 와 같다
        "stream": False,                       # 진행 상황을 조금씩 말고 한 번에 받는다
    }).encode(),

    # "보내는 내용이 JSON 형식이다" 라고 알려준다. 안 적으면 상대가 못 알아본다
    headers={"Content-Type": "application/json"},
)

# 이제 실제로 보낸다. timeout=180 은 "3분 기다려도 답이 없으면 포기한다"
with urllib.request.urlopen(req, timeout=180) as res:
    # read() 는 바이트로 받는다 -> decode() 로 사람이 읽는 글자로 되돌린다
    print(res.read().decode())
낯선 이름 풀이
이름무슨 뜻인가왜 여기 나오나
urllib파이썬 기본 인터넷 도구설치 없이 쓰려고
Request보낼 요청서아직 보내는 것은 아니다
urlopen실제로 보낸다여기서 통신이 일어난다
11434올라마의 문 번호올라마가 정해둔 값이라 바꿀 수 없다
.encode() / .decode()글자 ↔ 바이트컴퓨터끼리는 바이트로 오간다
timeout=180180초까지만 기다린다멈춘 채 매달리지 않으려고

전역에 깔린 것과 지우는 법

이번 편에서 늘어난 것
무엇어디에크기
올라마 프로그램설치 폴더수백 MB
받은 모델들C:\사용자\<내이름>\.ollama\models1.8GB
openai 패키지C:\Python313\Lib\site-packages몇 MB
제일 큰 것은 모델 파일이다. qwen-cpu 는 qwen2.5:3b 와 실물을 나눠 쓰므로 만들어도 거의 안 는다.
ollama list                  # 지금 있는 모델 목록
ollama rm qwen-cpu           # 만든 판을 지운다
ollama rm qwen2.5:3b         # 받아둔 모델까지 지운다
python -m pip uninstall openai

이번 편에 나온 것

정리
한 것내용
ollama --version설치됐는지 확인
ollama list / pull받아둔 모델 보기 / 받기
ModelfileFROM 한 줄 + 겹 수를 0으로 박는 PARAMETER num_gpu 0 한 줄
ollama create -f그래픽카드를 뗀 판을 만든다
ollama ps100% CPU 를 눈으로 확인한다
왜 새 판을 만드나요청마다 끄는 방식은 앞 배치를 재사용해 조용히 틀린다
config.py접속 설정을 한 군데로. 갈아탈 때 고칠 곳이 한 파일
messages · role대화 형태로 보낸다
속도초당 20~25토큰. 시간은 답 길이에 비례한다
채점 크기30명 (6~7분). 300명은 한 시간이 넘는다

미션

미션
  1. 01

    [필수] 내 노트북의 초당 토큰을 적는다

    mission_llm_1.py. 12_measure.py 를 돌려 초당 토큰을 적는다. ollama ps 의 100% CPU 화면도 같이 남긴다.

  2. 02

    [응용] 두 판을 나란히 비교한다

    mission_llm_2.py. qwen-cpu 와 qwen2.5:3b 에 같은 질문을 보내 초당 토큰을 비교한다. 원본 판이 에러가 나면 그것도 결과다 — 무슨 에러인지 적는다.

  3. 03

    [도전] 느린 것을 견디게 만든다

    mission_llm_3.py. 답이 13초 걸리면 화면이 멈춘 것처럼 보인다. stream=True 로 받아 글자가 흘러나오게 해본다. 총 시간은 그대로인데 체감이 어떻게 달라지나.

[도전] 총 시간은 같은데 왜 덜 답답한가충분히 고민해본 뒤 꼭 필요한 경우에만 열어보세요

첫 글자가 나오기까지의 시간이 체감을 정한다. 통째로 받으면 13초 동안 아무것도 없다가 한 번에 나오고, 흘려받으면 1~2초 만에 첫 글자가 나온다.

총 시간은 똑같다. 바뀐 것은 기다림의 모양뿐이다. 그런데 사람은 「멈춘 것 같다」와 「일하고 있다」를 완전히 다르게 느낀다.

느린 모델을 쓸 때 이게 특히 값을 한다. CPU 로 도는 우리에게는 사실상 필수다.

다만 뒤에서 이게 발목을 잡는다. 흘려보내면서 동시에 「형식을 지켜라」를 검사하려면 답이 다 와야 하기 때문이다. 그때 다시 이야기한다.

from config import client, MODEL

stream = client.chat.completions.create(
    model=MODEL,
    messages=[{"role": "user", "content": "화장품 추천 서비스를 세 문장으로 소개해줘"}],
    stream=True,                       # 통째로 말고 오는 대로 받는다
)

# 스트림은 조각이 하나씩 도착하는 것이라 for 문으로 받는다
for chunk in stream:
    # delta 는 "이번에 새로 온 부분" 이다. 빈 조각도 오므로 확인하고 찍는다
    piece = chunk.choices[0].delta.content
    if piece:
        # end="" 는 줄바꿈을 안 넣는다, flush=True 는 모아두지 말고 바로 화면에 뿌린다
        print(piece, end="", flush=True)

print()   # 마지막에 줄바꿈 하나

flush=True 를 빼면 파이썬이 글자를 모아뒀다 한 번에 뿌려서 흘려받는 의미가 없어진다. 자주 빠뜨리는 한 글자다.

이 코드는 다음 편의 상용 API 로 갈아타도 똑같이 돈다. stream=True 는 양쪽 다 지원하는 표준이라 config.py 만 바꾸면 그대로 동작한다.

정리하면

그래픽카드를 안 쓰기로 했다. 말썽을 부린 것이 모델도 코드도 아니고 그것이었기 때문이다. Modelfile 두 줄로 판을 하나 만들었고, ollama ps 에 100% CPU 가 찍히는 것을 눈으로 확인했다.

네 배 느려졌다. 그런데 느린 것은 계획에 넣을 수 있고 안 되는 것은 못 넣는다. 초당 2025토큰이라는 숫자를 재뒀으니, 뒤에서 채점을 30명으로 잡으면 67분이라는 것까지 지금 안다.

재면서 하나 배운 것도 있다. 걸린 시간은 10초에서 17초까지 흔들리는데 초당 토큰은 거의 일정했다. 느린 게 아니라 길게 답한 것이다. 그래서 답을 짧게 시키면 그만큼 빨라진다 — 뒤에서 형식을 고정할 때 이게 값을 한다.

그리고 config.py 를 만들었다. 접속 설정이 한 파일에만 있으니, 이 길이 끝내 안 맞으면 세 줄을 갈아끼워 다른 상대로 넘어간다. 나머지 코드는 한 글자도 안 바뀐다.

다음 편은 선택이다. 이 모델이 너무 느리거나 답이 시원찮으면 인터넷 너머의 좋은 모델을 빌려 쓸 수 있다. 대신 돈이 든다. 지금 이대로 괜찮으면 건너뛰고 8편으로 가면 된다 — 이후 편들은 전부 여기서 만든 qwen-cpu 를 그대로 쓴다.

Share
  • 파이썬
  • AI
  • Ollama
  • 세팅
  • OpenAI
모델을 얹는다 — 그래픽카드를 아예 안 쓰고 — 디코드랩(DCODELAB)