↓ メインコンテンツへスキップ

VLLM Ollama LLM

vLLM と Ollama の比較:ローカルLLMを用途でどう使い分けるか

vLLM と Ollama を両方使う立場からの比較です。一人で使うなら Ollama、GPU サーバーで大量のリクエストをさばくなら vLLM。ローカルLLMの導入、Docker・Kubernetes 構成、ハマりどころまで。

vLLM と Ollama の比較:ローカルLLMを用途でどう使い分けるか

一人で、あるいは一台のマシン上のひとつのアプリで、数分のうちにモデルを動かしたいなら Ollama を使ってください。GPU サーバーが大量のリクエストを同時にさばく必要があり、スループットが大事なら vLLM です。Ollama はモデルランナーです。量子化されたモデルを取ってきて、頼まれたときにロードし、使い終われば手放します。vLLM は推論サーバーです。起動時に GPU の大半を確保し、処理中のリクエストをすべてバッチにまとめて GPU を遊ばせません。どちらも OpenAI API を話すので、呼び出す側のコードはまったく同じにできます。

私はどちらも毎日使っています。Ollama はラップトップとオフィスの GPU ワークステーションで動いていて、開発と自分用のツールに使っています。vLLM はフリーランスの案件で、96 GB の GPU 一枚からチャットモデルを配信しています。最初は Docker Compose で、今は小さな Kubernetes クラスタの上です。この記事では、それぞれが何に向いているか、どうインストールするか、そして vLLM 側で時間を取られたことを書きます。

vLLM と Ollama の比較表
#

OllamavLLM
向いている用途一人で使う: ラップトップ、開発マシン、個人用のツール多数のユーザーや忙しいアプリ、GPU サーバー上で
動く環境macOS、Linux、Windows。Apple silicon、NVIDIA、AMD、または CPUNVIDIA GPU を積んだ Linux がいちばん楽。ほかのハードウェアでも手間をかければ動く
モデル独自ライブラリの量子化済み GGUF(qwen3:8b)Hugging Face の重み。BF16、FP8、AWQ、GPTQ(Qwen/Qwen3-8B)
インストールインストーラーを入れて ollama runpip install vllm、または Docker イメージ vllm/vllm-openai
GPU メモリ初回利用時にモデルをロードし、5 分間使われなければアンロード起動時にカードの一定割合(デフォルトで 90% 超)を確保し、そのまま保持
同時リクエスト並列スロットがいくつか連続バッチ処理(continuous batching)。これが本領
起動小さなモデルなら数秒大きなモデルなら数分
LoRA アダプターModelfile で新しいモデルに焼き込むサーバーひとつに複数載せ、リクエストごとに選ぶ
API独自 API に加えて、:11434/v1 で OpenAI 互換:8000/v1 で OpenAI 互換

Ollama とは
#

自分のマシンでモデルに答えさせる方法として、Ollama は私の知る限りいちばん簡単です。llama.cpp から生まれたもので、量子化された GGUF モデルを動かします。8B のモデルが約 5 GB に収まり、MacBook でも快適に動くのはそのためです。モデルを名前で pull すると、最初に呼び出したときにロードされ、5 分間何もすることがなければメモリから外れます。なので、ほかに動かしているものと行儀よくマシンを分け合ってくれます。

その行儀の良さがトレードオフです。Ollama は一度に一人を相手にする前提で作られています。いくつかのリクエストを並列に処理することはできますが、並列スロットひとつごとにメモリを食いますし、何十人ものユーザーを一枚の GPU にバッチでまとめるためのものでは、もともとありません。

vLLM とは
#

vLLM はサービングエンジンで、まさにそのバッチ処理のために作られています。それを支えているのは主に二つのアイデアです。PagedAttention は、リクエストごとの KV キャッシュ(モデルがトークンを読み書きしながら積み上げていく attention の状態)を小さな固定サイズのブロックに格納するので、パディングや断片化で GPU メモリが無駄になりません。連続バッチ処理では、新しいリクエストが今のバッチの終わりを待たずに、生成ステップの合間にバッチへ加わります。この二つのおかげで、vLLM を動かした GPU 一枚は、同じ GPU でリクエストを一件ずつ処理するよりはるかに多くの同時ユーザーをさばけます。

代償として、vLLM はカードを自分のものだと思っているかのように振る舞います。起動時に GPU メモリの取り分を確保し、重みに要らない分を KV キャッシュで埋め、誰も呼び出していなくても全部を握ったままです。それに、必要なのは Hugging Face のフルの重みか、FP8 のような GPU 向けの量子化で、Ollama が使う GGUF ファイルではありません。

API はひとつ、サーバーは二つ
#

どちらも OpenAI の chat completions API を公開しているので、私のコードは相手がどちらなのかを知りません。URL とモデル名は環境変数から取ります。

import os
from openai import OpenAI

# ラップトップ: LLM_URL=http://localhost:11434/v1  LLM_MODEL=qwen3:8b
# GPU サーバー: LLM_URL=http://localhost:8000/v1   LLM_MODEL=qwen3-8b
client = OpenAI(base_url=os.environ["LLM_URL"], api_key="not-needed")

reply = client.chat.completions.create(
    model=os.environ["LLM_MODEL"],
    messages=[{"role": "user", "content": "Say hello in Korean."}],
)
print(reply.choices[0].message.content)

大きな GPU のないラップトップでは、アプリは Ollama を向きます。サーバーでは vLLM を向きます。ほかは何も変わらず、GPU マシンの電源が入っていなくてもアプリを開発できます。vLLM 側では --served-model-name でモデルに短い名前を付けておけば、クライアントが Hugging Face のフルパスを送らずに済みます。

忘れやすいので、注意をひとつ。同じモデルでも、両者で同じ答えが返るわけではありません。Ollama の qwen3:8b は 4 ビット量子化ですが、vLLM では普通 16 ビットか FP8 の重みを動かします。片方に合わせて調整したプロンプトが、もう片方では違う振る舞いをすることがあります。なので測っているものは、回答の品質であれ、拒否であれ、JSON がパースできるかどうかであれ、本番で実際に使うサーバーで確かめる必要があります。

Ollama のインストール方法
#

Mac か Windows なら、ollama.com からアプリをダウンロードします。Linux ならこうです。

curl -fsSL https://ollama.com/install.sh | sh
ollama run qwen3:8b

これで終わりです。API は http://localhost:11434 で使えるようになっていて、OLLAMA_HOST を設定しない限り localhost でしか待ち受けません。それが望ましい挙動です。私の Ollama はオフィスのワークステーションで 127.0.0.1 にバインドして動いていて、どこからでもトンネル経由で届くようにしています(Cloudflare WARP でそれを組んだ方法)。

vLLM のインストール方法
#

NVIDIA の GPU と新しめのドライバーがある Linux なら、こうです。

python -m venv .venv && source .venv/bin/activate
pip install vllm
vllm serve Qwen/Qwen3-8B --served-model-name qwen3-8b --max-model-len 16384

初回の実行では Hugging Face から重みをダウンロードします。ログに Application startup complete と出たら、curl localhost:8000/v1/models でモデルが一覧に出て、上の Python がそのまま動きます。

Docker で vLLM を動かす
#

ずっと動いていてほしいものには、pip install ではなく公式イメージを使っています。主な理由は、CUDA、PyTorch、vLLM がセットで固定された状態で手に入るからです。汎用的な Compose のサービスはこうなります。

services:
  vllm:
    image: vllm/vllm-openai:v0.30.0   # タグを固定する。イメージは約 30 GB
    restart: unless-stopped
    ipc: host                          # PyTorch が共有メモリを必要とする
    command: >
      --model Qwen/Qwen3-8B
      --served-model-name qwen3-8b
      --gpu-memory-utilization 0.85
      --max-model-len 16384
    volumes:
      - ${HOME}/.cache/huggingface:/root/.cache/huggingface
    ports:
      - "127.0.0.1:8000:8000"          # LAN ではなく ssh -L 経由でアクセスする
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]
    healthcheck:
      test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
      interval: 10s
      timeout: 5s
      retries: 90
      start_period: 120s

タグは固定してください。vLLM は動きが速く、リリースのたびにフラグやデフォルトが変わります。それがいつ起きるかは、自分で決められるようにしておきたいところです。ipc: host(あるいは十分な shm_size)は vLLM 自身の Docker の手順に書かれています。長い start_period が必要な理由は、次の節のひとつ目です。

vLLM の運用でハマったこと
#

起動に数分かかる。 重みのロード、コンパイル、CUDA グラフのキャプチャで、私のサーバーでは 190 秒かかりました。大きなモデルなら 3〜5 分は普通です。Web アプリ向けに書いたヘルスチェックだと、準備ができる前に殺してしまいます。Compose なら長い start_period、Kubernetes なら startup probe が必要です。それに、再起動のたびにモデルのいない時間が数分できるので、気軽に再起動するものではありません。

GPU メモリは、与えた分を起動時に持っていく。 --gpu-memory-utilization は vLLM が使ってよいカードの割合で、最近のリリースのデフォルトは 0.92、重みに要らない分はすべて KV キャッシュになります。vLLM がカードを独り占めしているなら、それで問題ありません。私の場合はそうではなく、横で小さなモデルが動いているので、96 GB のカードで 0.70 にしています。モデルは FP8 の 35B の mixture-of-experts で、ロードすると 47.7 GiB、KV キャッシュには 15 GiB が残りました。16k のコンテキストなら十分な量です。最大シーケンス長が KV キャッシュに収まらないと言って vLLM が起動を拒むなら、大きな GPU を探しに行く前に --max-model-len を下げてみてください。

LoRA のスロットは先にメモリを取る。 vLLM はひとつのベースモデルに複数の LoRA アダプターを載せて配信でき、リクエストごとにモデル名で切り替えられます。これは Ollama には実質できないことです。ただし vLLM は起動時に、ランク --max-lora-rank のアダプター --max-loras 個分の領域を確保します。mixture-of-experts のモデルではこの確保がエキスパートにも及ぶので、すぐに膨らみます。0.70 では、ランク 64 のアダプター 4 つは収まりませんでした。2 つなら収まりました。

デフォルトのシーケンス上限がハイブリッドモデルに合わなかった。 私が配信しているモデルは attention の層に Mamba 系の層が混ざっていて、シーケンスごとにその層用のキャッシュブロックがひとつずつ必要です。そのカードでは vLLM のデフォルトの --max-num-seqs だと起動に失敗し、--max-num-seqs 256 で直りました。新しめのアーキテクチャが起動しないときは、もっと込み入った原因を疑う前にこのフラグを確認してください。

vllm という名前の Kubernetes Service で vLLM がクラッシュする。 これが個人的にいちばん気に入っています。Kubernetes は namespace 内のすべての Service について、その名前を元にした環境変数を注入します。なので vllm という Service があると、すべての Pod に VLLM_PORT=tcp://<cluster-ip>:8000 が渡ります。vLLM は VLLM_PORT を自分の設定のひとつとして読み、そこにある URL を解釈できずに落ちました。もちろん、モデルを丸ごとロードしたあとでです。なので試すたびに 3 分かかりました。vLLM のドキュメントにはこの注意がちゃんと書いてあって、私はそれを後から見つけました。直し方は、Pod に enableServiceLinks: false を付けるか、Service を別の名前にすることです。

動く Deployment の骨格を、大事な部分だけに絞って載せます。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm
spec:
  replicas: 1
  strategy:
    type: Recreate               # 先に古い Pod が GPU を解放する必要がある
  selector:
    matchLabels: {app: vllm}
  template:
    metadata:
      labels: {app: vllm}
    spec:
      enableServiceLinks: false  # Service 由来の VLLM_PORT=tcp://... を入れない
      containers:
        - name: vllm
          image: vllm/vllm-openai:v0.30.0
          args: ["--model=Qwen/Qwen3-8B", "--served-model-name=qwen3-8b"]
          resources:
            limits:
              nvidia.com/gpu: "1"
              memory: 32Gi       # ロード時は配信時よりはるかに多くのホスト RAM が要る
          volumeMounts:
            - {name: shm, mountPath: /dev/shm}
          startupProbe:          # モデルがロードされるまでは判定しない
            httpGet: {path: /health, port: 8000}
            initialDelaySeconds: 120
            periodSeconds: 10
            failureThreshold: 90
          livenessProbe:         # 再起動はフルリロードになるので気長に待つ
            httpGet: {path: /health, port: 8000}
            periodSeconds: 30
            timeoutSeconds: 5
            failureThreshold: 10
      volumes:
        - name: shm
          emptyDir: {medium: Memory, sizeLimit: 16Gi}

真似する価値があるのはプローブの部分です。startup probe は 2 分待ってから、さらに 15 分、モデルが上がってくるのを待ち、それでもだめなら Kubernetes が死んだと判断します。liveness probe が再起動するのは、失敗が 5 分ほど続いてからです。負荷の高いエンジンでは /health が遅くなることがあり、どのみち再起動はフルリロードになるからです。GPU 一枚の環境では Recreate が効いてきます。ローリングアップデートは新しい Pod を先に起動するので、新しい Pod は古い Pod が握ったままの GPU を待って止まってしまいます。メモリの上限が大きく見えるのは、実際に大きいからです。重みが GPU に載ってしまえば、私の環境ではホスト RAM は約 7 GiB に落ち着きますが、ロードそのものにはもっとずっと多く必要です。

Ollama について知っておきたいこと
#

驚かされることは少なめです。主に、やっていることが少ないからです。

しばらく静かだったあとの最初のリクエストは遅くなります。モデルがアンロードされていて、もう一度ロードしなければならないからです。OLLAMA_KEEP_ALIVE は、最後のリクエストのあとモデルがどれだけメモリに残るかを決めます。その待ち時間が気になるなら値を上げるか、ほかに何もしないマシンなら -1 にしてください。

コンテキストウィンドウは GPU メモリの量で変わります。24 GiB 未満だと Ollama のデフォルトは 4k トークンで、ほとんどのモデルが対応している長さよりずっと短く、収まらないプロンプトはエラーにならずに黙って切り詰められます。長いプロンプトの冒頭が無視されているように見えたら、OLLAMA_CONTEXT_LENGTH(またはリクエストごとの num_ctx)を実際に必要な長さに設定してください。

リモートの Ollama をラップトップにフォワードするなら、11434 は避けてください。Ollama のデフォルトのポートなので、ローカルの Ollama がいつか起動すると二つがポートを奪い合い、気づかないまま違うほうと話していた、ということになりかねません。私は 11435 にフォワードしています。

それから、Ollama はマルチユーザーのサーバーではありません。数人が使う社内ツールなら問題ありませんが、本当に同時アクセスが出てきたら、そこが vLLM に移るタイミングです。

必要な GPU メモリの目安
#

重みだけのおおよそのサイズです。

モデルサイズ16 ビット(BF16)8 ビット(FP8)4 ビット GGUF(Q4_K_M)
8B~16 GB~8 GB~5 GB
32B~64 GB~32 GB~20 GB
70B~140 GB~70 GB~43 GB

16 ビットならパラメータあたり約 2 バイト、8 ビットなら 1 バイト、Q4_K_M なら 0.5 バイトを少し超えるくらいです。重みに加えて KV キャッシュが乗り、これはコンテキスト長と処理中のリクエスト数に応じて増えます。同じモデルでも、50 人に配信する vLLM のほうが、あなた一人に応える Ollama よりずっと多くのメモリを必要とするのは、この二つ目の部分のせいです。

どちらを使うか
#

自分のマシンでモデルを動かしたいなら、Ollama をインストールしてそこで終わりにしてください。10 分もあれば何かが答えてくれるようになります。GPU サーバーがあって、それを一日中呼び出すユーザーやアプリがいるなら vLLM を使い、遅い起動と持っていかれるメモリを受け入れて、プローブをきちんと設定してください。結局両方を使う構成も多く、私のところもそうです。コードを書く場所には Ollama、リクエストが届く場所には vLLM です。

ほかの選択肢もあります。vLLM の代わりによく挙がるのは SGLang ですが、本番で動かしたことはないので、推測で書くよりは外しておきました。モデルをスマホで動かす必要があるなら、それはまた llama.cpp の出番で、これについては React Native でオンデバイスの LLM を動かす記事に書きました。

コメント

コメントは確認後に表示されます。