松実のStudy Knowledge

Windows + WSL2 + RTX 2070 SuperでvLLMをLAN内OpenAI互換APIとして使う手順

やりたかったこと

手元のWindows 11 PCに載っているRTX 2070 Superを使い、LAN内の別マシンからOpenAI SDK互換のAPIとして呼び出せるローカルLLMサーバーを立てました。 vLLMのOpenAI互換サーバーをWSL2 Ubuntu上で起動し、別PCから /v1/models/v1/chat/completions にアクセスできるところまでを目標にしています。

  • Windows 11 + WSL2上でvLLMを動かす
  • RTX 2070 Super 8GBの範囲で無理なく起動する
  • LAN内の別マシンからOpenAI互換APIとして利用する
  • 公開範囲はローカルLANのみにする

※この記事では、実際のLAN IP、Windows / WSLユーザー名、APIキーなどの固有情報は記事用にマスクしています。 APIキーのサンプル値はそのまま使わず、必要に応じて変更してください。

環境

  • ホストOS: Windows 11
  • 実行環境: WSL2 Ubuntu
  • GPU: NVIDIA GeForce RTX 2070 Super 8GB
  • APIサーバー: vLLM OpenAI互換API
  • vLLM: 0.20.1
  • PyTorch側CUDA: 13.0
  • CUDA Toolkit: /usr/local/cuda-13.0
  • 初回モデル: Qwen/Qwen2.5-1.5B-Instruct
  • APIポート: 8000
Windows / WSL ユーザー: <USER>
vLLM PC の LAN IP: <VLLM_PC_LAN_IP>
別マシン: <CLIENT_PC>
API キー: local-vllm-key

構成図

WSL2はmirrored networkingにし、WSL側のvLLMを 0.0.0.0:8000 で待ち受けます。 LAN内の別マシンは、vLLM PCのLAN IP宛てにOpenAI互換APIとしてアクセスします。

Windows 11 と WSL2 と vLLM を使った LAN 内 OpenAI 互換 API 構成図
[LAN内の別マシン]
  OpenAI SDK / curl
        |
        | http://<VLLM_PC_LAN_IP>:8000/v1
        v
[Windows 11 PC]
  ├─ Windows Firewall / Hyper-V Firewall
  ├─ NVIDIA Driver
  └─ WSL2 Ubuntu (mirrored networking)
       ├─ Python venv
       ├─ CUDA Toolkit 13.0
       └─ vLLM OpenAI compatible server
            └─ Qwen/Qwen2.5-1.5B-Instruct

WSL2のネットワークをmirroredにする

LAN内の別マシンからWSL2上のサーバーへ入りやすくするため、Windows側の .wslconfig でmirrored networkingを有効にしました。 PowerShellでWindowsのユーザーフォルダに設定ファイルを作ります。 今回はWindowsホストとWSL2が同じLAN側IPを共有する構成にし、vLLMは 0.0.0.0 で待ち受ける形にしました。

@"
[wsl2]
networkingMode=mirrored
hostAddressLoopback=true
"@ | Set-Content -Encoding ASCII "$env:USERPROFILE\.wslconfig"

反映します。

wsl --shutdown
wsl

WSL側でLAN IPが見えているか確認します。

ip address

eth1 などに <VLLM_PC_LAN_IP>/24 が付いていれば、WSLがLAN側のアドレスを持っている状態です。 この状態でもWindows FirewallとHyper-V Firewallの許可は別途必要です。

vLLM用のPython環境を作る

以降はWSL Ubuntu側で作業します。

sudo apt update
sudo apt install -y python3 python3-venv python3-pip

mkdir -p ~/vllm-server
cd ~/vllm-server

python3 -m venv .venv
source .venv/bin/activate

pip install --upgrade pip
pip install vllm

GPUが見えているか確認します。

nvidia-smi

PyTorchが使っているCUDAバージョンも確認しました。

python -c "import torch; print(torch.version.cuda)"

今回の環境では 13.0 でした。

CUDA Toolkit 13.0を入れる

vLLM起動時、FlashInferのJITビルドで nvcc が必要になりました。 そのため、WSL内にCUDA Toolkit 13.0を入れています。

sudo apt update
sudo apt install -y wget gnupg build-essential

wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/x86_64/cuda-keyring_1.1-1_all.deb
sudo dpkg -i cuda-keyring_1.1-1_all.deb

sudo apt update
sudo apt install -y cuda-toolkit-13-0

環境変数を設定します。

echo 'export CUDA_HOME=/usr/local/cuda-13.0' >> ~/.bashrc
echo 'export PATH=$CUDA_HOME/bin:$PATH' >> ~/.bashrc
echo 'export LD_LIBRARY_PATH=$CUDA_HOME/lib64:${LD_LIBRARY_PATH}' >> ~/.bashrc

source ~/.bashrc

確認します。

which nvcc
nvcc --version
nvidia-smi

WSL内にLinux版NVIDIAドライバは入れません。 WSL2ではWindows側のNVIDIAドライバを使います。

vLLMを起動する

RTX 2070 Super 8GBでは、vLLMのデフォルト設定のままだとVRAM使用量が厳しく、起動時に失敗しました。 そのため --gpu-memory-utilization を下げ、--max-model-len も2048にしています。

cd ~/vllm-server
source .venv/bin/activate

export CUDA_HOME=/usr/local/cuda-13.0
export PATH=$CUDA_HOME/bin:$PATH
export LD_LIBRARY_PATH=$CUDA_HOME/lib64:${LD_LIBRARY_PATH}

vllm serve Qwen/Qwen2.5-1.5B-Instruct \
  --host 0.0.0.0 \
  --port 8000 \
  --dtype float16 \
  --max-model-len 2048 \
  --gpu-memory-utilization 0.70 \
  --api-key local-vllm-key

起動成功時は、次のようなログが出ます。

Starting vLLM server on http://0.0.0.0:8000
Application startup complete.

WSL内で疎通確認する

今回のmirrored mode環境では、127.0.0.1 ではなくLAN IP宛てで疎通確認しました。

curl http://<VLLM_PC_LAN_IP>:8000/v1/models \
  -H "Authorization: Bearer local-vllm-key"

Windows Firewall / Hyper-V Firewallを許可する

WSL mirrored networkingでは、通常のWindows FirewallだけでなくHyper-V Firewall側の許可も必要でした。 vLLM PCのWindows側で、管理者PowerShellから実行します。

WSLのVMCreatorIdは以下で確認できます(VMCreatorNameWSL の行の値を使います)。

Get-NetFirewallHyperVVMCreator
New-NetFirewallHyperVRule `
  -Name "vLLM-WSL-8000" `
  -DisplayName "vLLM WSL 8000" `
  -Direction Inbound `
  -VMCreatorId "{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}" `
  -Protocol TCP `
  -LocalPorts 8000 `
  -Action Allow

通常のWindows Firewallも許可します。

New-NetFirewallRule `
  -DisplayName "vLLM LAN 8000" `
  -Direction Inbound `
  -Action Allow `
  -Protocol TCP `
  -LocalPort 8000 `
  -Profile Any

別マシンから疎通確認する

LAN内の別マシンから、まずモデル一覧が取れるか確認します。

curl.exe http://<VLLM_PC_LAN_IP>:8000/v1/models -H "Authorization: Bearer local-vllm-key"

チャット生成まで確認する場合は、次のように投げます。

curl.exe http://<VLLM_PC_LAN_IP>:8000/v1/chat/completions `
  -H "Authorization: Bearer local-vllm-key" `
  -H "Content-Type: application/json" `
  -d "{\"model\":\"Qwen/Qwen2.5-1.5B-Instruct\",\"messages\":[{\"role\":\"user\",\"content\":\"こんにちは。短く自己紹介して。\"}],\"max_tokens\":128}"

OpenAI SDKから使う

OpenAI SDKからは base_url をvLLMの /v1 に向け、APIキーには起動時に指定した値を渡します。 クライアント側の model も、vLLMで起動したモデル名と合わせます。

from openai import OpenAI

client = OpenAI(
    base_url="http://<VLLM_PC_LAN_IP>:8000/v1",
    api_key="local-vllm-key",
)

res = client.chat.completions.create(
    model="Qwen/Qwen2.5-1.5B-Instruct",
    messages=[
        {"role": "user", "content": "こんにちは。短く返答して。"}
    ],
    max_tokens=128,
)

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

別のLLMに変える

モデルを変える場合は、基本的には vllm serve のモデル名を変更します。 RTX 2070 Super 8GBでは、まず1.5Bから4B級が扱いやすく、7B級は量子化モデルや短めのコンテキスト長を検討するのが良さそうです。

vllm serve <MODEL_NAME> \
  --host 0.0.0.0 \
  --port 8000 \
  --dtype float16 \
  --max-model-len 2048 \
  --gpu-memory-utilization 0.70 \
  --api-key local-vllm-key

クライアント側の model も同じ名前にします。

res = client.chat.completions.create(
    model="<MODEL_NAME>",
    messages=[
        {"role": "user", "content": "こんにちは"}
    ],
)

起動スクリプトで運用する

毎回コマンドを打つのは大変なので、Windows側に start-vllm.ps1 を作ってPowerShellから起動できるようにしました。 パラメーターを変えればモデルやポートも差し替えられます。

param(
    [string]$Model = "Qwen/Qwen2.5-1.5B-Instruct",
    [string]$ApiKey = "local-vllm-key",
    [int]$Port = 8000,
    [int]$MaxModelLen = 2048,
    [double]$GpuMemoryUtilization = 0.70,
    [string]$CudaHome = "/usr/local/cuda-13.0"
)

通常起動は次の形です。

.\start-vllm.ps1

実行ポリシーで止まる場合は、次のように実行します。

powershell -ExecutionPolicy Bypass -File .\start-vllm.ps1

設定を変えて起動する例です。

.\start-vllm.ps1 `
  -Model "Qwen/Qwen2.5-1.5B-Instruct" `
  -Port 8000 `
  -MaxModelLen 2048 `
  -GpuMemoryUtilization 0.70 `
  -ApiKey "local-vllm-key"

PC起動時にタスクスケジューラで常駐させる

PC起動時にvLLMを常駐させたい場合は、Windowsのタスクスケジューラを使います。 Startupフォルダー方式はログイン後にしか動かないため、今回は起動用に install-vllm-scheduled-task.ps1 を作成しました。

登録します。

powershell -ExecutionPolicy Bypass -File .\install-vllm-scheduled-task.ps1

この場合、タスクはPC起動時に登録ユーザーの対話ログオン環境で起動します。 より確実に「ユーザーがログオンしていなくても実行」したい場合は、Windows資格情報を登録する方式にします。

powershell -ExecutionPolicy Bypass -File .\install-vllm-scheduled-task.ps1 -RunWhetherUserIsLoggedOnOrNot

この方式では、タスク登録時にWSLのvLLM環境を作ったWindowsユーザーの資格情報を入力します。 WSLはユーザー単位の環境なので、別ユーザーやSYSTEMで動かすより、vLLM環境を作ったユーザーで動かすのが安全です。

タスクの確認です。

Get-ScheduledTask -TaskName "vLLM WSL API Server"
Get-ScheduledTaskInfo -TaskName "vLLM WSL API Server"

手動実行もできます。

Start-ScheduledTask -TaskName "vLLM WSL API Server"

解除する場合です。

powershell -ExecutionPolicy Bypass -File .\uninstall-vllm-scheduled-task.ps1

PC起動直後はWSL、GPU、ネットワークが初期化しきっていないことがあります。 そのため、start-vllm.ps1 側の -StartupDelaySeconds で60秒待ってから起動するようにしました。

-StartupDelaySeconds 60

タスクスケジューラからの非対話実行では画面が見えないことがあるため、ログ出力は必須です。 ログは vllm-startup.log に出します。

ただし、最初はタスクスケジューラから start-vllm.ps1 を呼んでも安定しませんでした。 PowerShellからWSLへ複数行の bash -lc コマンドを渡すとクォートが崩れ、vLLMが起動しないことがあったためです。

syntax error: unexpected end of file

そのため、Windows側の start-vllm.ps1 と、WSL側で実際にvLLMを起動する start-vllm-wsl.sh に分けました。 Windows側はタスクスケジューラから呼ばれ、起動前の待機、ログ出力、WindowsパスからWSLパスへの変換、モデル名やAPIキーなどの引数渡しを担当します。 WSL側は ~/vllm-server へ移動し、仮想環境とCUDAの設定を整えてからvLLMを起動します。

vllm コマンドをPATHから探すのではなく、venv内の実体を直接呼ぶようにしました。 これで自動起動時にも、手動起動時と同じ環境で起動しやすくなります。

exec .venv/bin/vllm serve "$MODEL" \
  --host 0.0.0.0 \
  --port "$PORT" \
  --dtype float16 \
  --max-model-len "$MAX_MODEL_LEN" \
  --gpu-memory-utilization "$GPU_MEMORY_UTILIZATION" \
  --api-key "$API_KEY"

手動起動用には start-vllm.bat も作成しました。

@echo off
setlocal

chcp 65001 >nul
cd /d "%~dp0"
powershell.exe -NoExit -ExecutionPolicy Bypass -File "%~dp0start-vllm.ps1" %*

endlocal

これで次のように起動できます。

.\start-vllm.bat

引数もそのまま渡せます。

.\start-vllm.bat -StartupDelaySeconds 0 -Port 8000

ハマったところ

PowerShellのcurlはcurlではない

PowerShellの curlInvoke-WebRequest のエイリアスなので、Linuxや curl.exe-H オプションと挙動が違います。 PowerShellでは curl.exe を明示するのが簡単でした。

curl.exe http://<VLLM_PC_LAN_IP>:8000/v1/models -H "Authorization: Bearer local-vllm-key"

vLLMがVRAMを取りすぎて起動しない

最初は次のようなエラーで落ちました。

Free memory on device cuda:0 (...) is less than desired GPU memory utilization (0.92, ...)

vLLMのデフォルトVRAM使用率が高く、Windows表示などで既にVRAMが使われていたためです。 今回は次の指定で回避しました。

--gpu-memory-utilization 0.70
--max-model-len 2048

RTX 2070 SuperではFlashAttention 2が使えない

ログに次のようなメッセージが出ました。

Cannot use FA version 2 is not supported due to FA2 is only supported on devices with compute capability >= 8

RTX 2070 SuperはTuring世代でCompute Capability 7.5なので、FlashAttention 2の対象外です。 ただしvLLMは別backendに切り替えて動作できました。

FlashInferがnvccを要求した

次のエラーが出ました。

RuntimeError: Could not find nvcc and default cuda_home='/usr/local/cuda' doesn't exist

FlashInferのJITビルドに nvcc が必要だったため、WSL内に cuda-toolkit-13-0 を入れて解決しました。

WSL mirrored modeではlocalhostが期待通りでないことがある

vLLMは 0.0.0.0:8000 で待ち受けていましたが、WSL内の 127.0.0.1:8000 やWindowsホスト自身からの <VLLM_PC_LAN_IP>:8000 が通らない場面がありました。 実際には、WSL内から <VLLM_PC_LAN_IP>:8000 は通り、LAN内の別マシンからも同じLAN IP宛てで通りました。 今回の目的はLAN内の別マシンから使うことなので、クライアント側では http://<VLLM_PC_LAN_IP>:8000/v1 を使う形にしています。

ネットワーク設定はmirrored / LAN IP / Firewallをセットで見る

mirrored networkingを有効にしただけでは、LAN内の別マシンから必ず到達できるわけではありませんでした。 WSL側にLAN IPが見えていること、vLLMが 0.0.0.0:8000 で待ち受けていること、Windows FirewallとHyper-V Firewallの両方で 8000/tcp を許可していることをセットで確認する必要がありました。

Hyper-V Firewallの許可が必要だった

Windows 11のWSL mirrored networkingでは、通常のWindows FirewallだけでなくHyper-V Firewallも許可が必要でした。

Hyper-V Firewallとは

Hyper-V Firewallは、Windowsの仮想ネットワークスイッチ(vSwitch)レベルで動作するファイアウォールです。 通常のWindows FirewallがOSのネットワーク層でパケットを制御するのに対し、Hyper-V Firewallは仮想スイッチを通過するパケットを制御します。 WSL2は内部的にHyper-Vの軽量VMとして動作しているため、LANからWSL2上のサーバーへ到達するにはこのHyper-V Firewallも通過する必要があります。 mirroredモードではWSLとWindowsホストが同じIPを共有するため、この経路が特に問題になりやすいです。

VMCreatorIdの確認方法

-VMCreatorId にはWSL用の固定GUIDを指定します。以下のPowerShellコマンドで自分の環境の値を確認できます。

Get-NetFirewallHyperVVMCreator

出力の一覧から VMCreatorNameWSL の行を探し、その VMCreatorId の値を使います。

追加したルールの確認・削除

追加したHyper-V Firewallルールは以下で一覧確認できます。

Get-NetFirewallHyperVRule

不要になった場合は以下で削除できます。

Remove-NetFirewallHyperVRule -Name "vLLM-WSL-8000"

Windowsホスト自身からは失敗しても、別マシンからは成功した

vLLM PC自身から <VLLM_PC_LAN_IP>:8000 へアクセスすると失敗する一方、LAN内の別マシンからは成功しました。 Windowsホスト自身からmirrored WSLのLAN IPへ戻る経路だけが特殊だったと考えています。 実運用では別マシンから使うため問題ありませんでした。

自動起動ではPowerShellとWSLの役割分担が必要だった

タスクスケジューラからの自動起動では、手動起動時と違ってカレントディレクトリ、PATH、文字コード、ログファイルの状態が揃わないことがありました。 そのためWindows側のPowerShellは起動の入口とログ管理に寄せ、WSL側のシェルスクリプトに実際のvLLM起動処理を寄せる構成にしました。

自動起動や手動起動を繰り返すと、前回のPowerShellプロセスが vllm-startup.log を掴んだままになり、起動前に落ちることもありました。 ログファイルがロックされていた場合は、タイムスタンプ付きの別名ログへ逃がすようにしています。

$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$LogPath = Join-Path $logDirectory "$logBaseName-$timestamp-$PID$logExtension"

PowerShell経由のログや日本語リクエストが文字化けする

Windows PowerShell経由でWSLのvLLMログを見ると、vLLMのバナーや日本語が文字化けすることがありました。 Windows側ではコンソールとPowerShellの入出力をUTF-8へ寄せます。

$utf8NoBom = New-Object System.Text.UTF8Encoding $false
[Console]::InputEncoding = $utf8NoBom
[Console]::OutputEncoding = $utf8NoBom
$OutputEncoding = $utf8NoBom
chcp.com 65001 > $null

Batch側でもコードページをUTF-8にしました。

chcp 65001 >nul

WSL側の起動スクリプトでは、localeとPythonの入出力をUTF-8に寄せました。

export LANG="${LANG:-C.UTF-8}"
export LC_ALL="${LC_ALL:-C.UTF-8}"
export PYTHONIOENCODING="${PYTHONIOENCODING:-utf-8}"

PowerShellから日本語を含むAPIリクエストを送る場合は、curl.exe より Invoke-RestMethod の方が安定しました。

$body = @{
  model = "Qwen/Qwen2.5-1.5B-Instruct"
  messages = @(
    @{
      role = "user"
      content = "こんにちは。短く自己紹介して。"
    }
  )
  max_tokens = 128
} | ConvertTo-Json -Depth 5

$response = Invoke-RestMethod `
  -Uri "http://<VLLM_PC_LAN_IP>:8000/v1/chat/completions" `
  -Method Post `
  -Headers @{ Authorization = "Bearer local-vllm-key" } `
  -ContentType "application/json; charset=utf-8" `
  -Body $body

$response.choices[0].message.content

まとめ

WSL2のmirrored networking、Windows / Hyper-V Firewall、vLLMのVRAM設定を揃えることで、Windows 11 PCをLAN内のOpenAI互換APIサーバーとして使えるようになりました。 RTX 2070 Super 8GBでも、小さめのinstructモデルから始めればローカルAPIの動作確認には十分使えます。

Advertisement