0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

vLLM × Qwen3 × cline:cline生成コードでCIFAR-10学習もできるローカルLLM環境の構築例

Posted at

1. はじめに

前回、vLLMを使用したLLMサーバーを構築しました。

その応用でLLMサーバーをclineと連携しましたので、記事を作成しました。

2. この記事に記載してあること

  • Dockerを使用したvLLMサーバー
  • clineの設定
  • clineでのコード生成例
  • 生成したCIFAR-10コードの動かし方

3. vLLMサーバーの構築

今回工夫した点

基本的には、下記と同じことを実施しています。

追加として、「bitsandbytes」を用いた4 bit量子化モデルを使用しています。

構築手順

  1. 以下のようなrequirements.vllmを作成します。

    bitsandbytes
    
  2. 以下の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 )をダウンロードしています。

  3. 以下のような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の設定

  1. VSCodeでClineプラグインをインストール

  2. 以下の設定を実施

    項目 設定値
    API Provider OpenAI Compatible
    Base URL http://localhost:8000/v1
    API Key 任意の文字列(ローカル環境のため制限なし)
    Model /models/Qwen3-14B-bnb-4bit
  3. 設定を保存すれば、Cline経由でvLLMを使用してホストしたQwen3が使えるようになります。

5. clineで生成したCIFAR-10学習コードの実行

  1. 以下のコマンドで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%
    
  2. 学習終了後は以下のコマンドで後片付けを実施

    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)
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?