1. はじめに
前回、vLLMを使用したLLMサーバーを構築しました。
その応用でLLMサーバーをclineと連携しましたので、記事を作成しました。
2. この記事に記載してあること
- Dockerを使用したvLLMサーバー
- clineの設定
- clineでのコード生成例
- 生成したCIFAR-10コードの動かし方
3. vLLMサーバーの構築
今回工夫した点
基本的には、下記と同じことを実施しています。
追加として、「bitsandbytes」を用いた4 bit量子化モデルを使用しています。
構築手順
-
以下のようなrequirements.vllmを作成します。
bitsandbytes
-
以下のDockerfileを作成します。
FROM nvidia/cuda:12.8.1-cudnn-devel-ubuntu22.04 COPY ./requirements/requirements.vllm ./ RUN apt-get update && \ apt-get install -y python3 python3-pip python3-venv && \ pip3 install --upgrade pip RUN pip install -r requirements.vllm && \ pip install vllm==0.8.5 --extra-index-url https://download.pytorch.org/whl/cu128 && \ pip install flashinfer-python -i https://flashinfer.ai/whl/cu126/torch2.6/ && \ huggingface-cli download --local-dir=/models/Qwen3-14B-bnb-4bit unsloth/Qwen3-14B-bnb-4bit RUN groupadd -r vllmuser && \ useradd -r -g vllmuser -m -d /home/vllmuser vllmuser USER vllmuser
unslothがホストしている 4bit 量子化を実施した 14 Bのモデル( https://huggingface.co/unsloth/Qwen3-14B-bnb-4bit )をダウンロードしています。
-
以下のようなcompose.ymlを作成します。
services: vllm: build: context: ./docker/ dockerfile: DockerFile.vllm command: "vllm serve /models/Qwen3-14B-bnb-4bit --quantization bitsandbytes --host 0.0.0.0" deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] ports: - "8000:8000"
ここで、
vllm serve
コマンドにおける--quantization bitsandbytes
を指定することでbitsandbytesを使用したモデルを使用することが出来るようになります。
4. clineの設定
-
VSCodeでClineプラグインをインストール
-
以下の設定を実施
項目 設定値 API Provider OpenAI Compatible Base URL http://localhost:8000/v1
API Key 任意の文字列(ローカル環境のため制限なし) Model /models/Qwen3-14B-bnb-4bit
-
設定を保存すれば、Cline経由でvLLMを使用してホストしたQwen3が使えるようになります。
5. clineで生成したCIFAR-10学習コードの実行
-
以下のコマンドでCIFAR-10の学習を実施する。
コマンド:
docker compose run -it --rm gpu-python-app python main.py
ログ出力例:
Epoch 1/100, Loss: 1.5380 Epoch 2/100, Loss: 1.0142 ... Epoch 100/100, Loss: 0.0296 Test Accuracy: 94.01%
-
学習終了後は以下のコマンドで後片付けを実施
docker compose down
6. 感想
- clineに命令をするだけで簡単な画像分類モデルを作成出来たので便利だと感じました。
- 一方で、細かい命令などはなかなか思い通りにいかないので本格的に使用するのであれば、下記が考えられると思います。
- より大きなモデルを使用する(mac studioなどがいい?)。
- おとなしく性能の良いクローズドモデルに課金する
7. 使用コード
8. 生成したコード(補足)
初期プロンプトとして、下記を与えました。
main.pyを作成して、pytorchでcifar10を学習させるコードを作成して
結果として、下記のようなコードが生成されました。
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
# Hyperparameters
EPOCHS = 10
BATCH_SIZE = 64
LEARNING_RATE = 0.001
# Device configuration
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# Data loading and preprocessing
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
train_dataset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
test_dataset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False)
# Simple CNN model
class SimpleCNN(nn.Module):
def __init__(self):
super(SimpleCNN, self).__init__()
self.model = nn.Sequential(
nn.Conv2d(3, 32, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Conv2d(32, 64, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Flatten(),
nn.Linear(64 * 8 * 8, 512),
nn.ReLU(),
nn.Linear(512, 10)
)
def forward(self, x):
return self.model(x)
model = SimpleCNN().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)
# Training loop
for epoch in range(EPOCHS):
model.train()
running_loss = 0.0
for inputs, labels in train_loader:
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item() * inputs.size(0)
epoch_loss = running_loss / len(train_dataset)
print(f"Epoch {epoch+1}/{EPOCHS}, Loss: {epoch_loss:.4f}")
# Evaluation
model.eval()
correct = 0
total = 0
with torch.no_grad():
for inputs, labels in test_loader:
inputs, labels = inputs.to(device), labels.to(device)
outputs = model(inputs)
predicted = torch.argmax(outputs, dim=1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print(f"Test Accuracy: {100 * correct / total:.2f}%")
初回にしては悪くはないように思います。
その後、下記を依頼しました。
- データ拡張の導入
- モデルの改修
- 学習率スケジューラーの導入
下記のような出力が得られました。
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
# Hyperparameters
EPOCHS = 100
BATCH_SIZE = 128
LEARNING_RATE = 0.01
# Device configuration
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# Data loading and preprocessing with enhanced augmentation
train_transform = transforms.Compose([
transforms.RandomHorizontalFlip(),
transforms.RandomCrop(32, padding=4),
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),
transforms.RandomErasing(p=0.2, scale=(0.02, 0.33), ratio=(0.3, 3.0))
])
test_transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
train_dataset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=train_transform)
test_dataset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=test_transform)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=2)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=2)
# ResNet-18 adapted for 32x32 input size
class ResNet18_CIFAR(nn.Module):
def __init__(self, num_classes=10):
super(ResNet18_CIFAR, self).__init__()
# Initial convolutional layer (modified for 32x32 input)
self.conv1 = nn.Conv2d(3, 64, kernel_size=3, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU(inplace=True)
# Residual blocks (adjusted for 32x32 input)
self.layer1 = self._make_layer(64, 64, 2)
self.layer2 = self._make_layer(64, 128, 2, stride=2)
self.layer3 = self._make_layer(128, 256, 2, stride=2)
self.layer4 = self._make_layer(256, 512, 2, stride=2)
# Final layers
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512, num_classes)
def _make_layer(self, in_channels, out_channels, blocks, stride=1):
"""Create a residual layer with specified number of blocks"""
layers = []
layers.append(ResidualBlock(in_channels, out_channels, stride))
for _ in range(1, blocks):
layers.append(ResidualBlock(out_channels, out_channels))
return nn.Sequential(*layers)
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.avgpool(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
# Residual block definition
class ResidualBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride=1):
super(ResidualBlock, self).__init__()
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3,
stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3,
stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU(inplace=True)
# Downsample if input and output dimensions differ
if stride != 1 or in_channels != out_channels:
self.downsample = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(out_channels)
)
else:
self.downsample = None
def forward(self, x):
residual = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
if self.downsample is not None:
residual = self.downsample(x)
out += residual
out = self.relu(out)
return out
model = ResNet18_CIFAR().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=LEARNING_RATE, momentum=0.9)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS, eta_min=1e-6)
def train_model(model, train_loader, optimizer, scheduler, device, epochs=EPOCHS):
criterion = nn.CrossEntropyLoss()
for epoch in range(epochs):
model.train()
running_loss = 0.0
for inputs, labels in train_loader:
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item() * inputs.size(0)
scheduler.step()
epoch_loss = running_loss / len(train_dataset)
print(f"Epoch {epoch+1}/{epochs}, Loss: {epoch_loss:.4f}")
return model
def evaluate_model(model, test_loader, device):
model.eval()
correct = 0
total = 0
with torch.no_grad():
for inputs, labels in test_loader:
inputs, labels = inputs.to(device), labels.to(device)
outputs = model(inputs)
predicted = torch.argmax(outputs, dim=1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
accuracy = 100 * correct / total
print(f"Test Accuracy: {accuracy:.2f}%")
return accuracy
# Train and evaluate
model = train_model(model, train_loader, optimizer, scheduler, device)
evaluate_model(model, test_loader, device)