2
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?

8GBVRAMでQwen3-Coder-30B-A3Bを動かしてみた

2
Posted at

8GB VRAMでは、大きなモデルを運用できない……

ローカルでLLMを使うときに重要なパラメータがVRAMの容量です。
私のPCは、GeForce RTX 4060でVRAMの容量は 8GB しかありません。せめて12GB、できたら16GBあれば、大きなモデルが動かせるのにと思う毎日でした。
高性能なGPUは、価格と消費電力が大きいですし、統合メモリーのMac miniも大容量RAM機種はなくなったみたい(?)で、AMDのmini PCを買ってまでも……
そんなとき、@DanielOkadaさんの、GPUだけでLLMを動かす必要はないという記事を見て、これはいける!と感じ、私も試してみました。

ハードウエア&ソフトウエア

項目 内容
CPU Corei9 13900
RAM 32GB
GPU GeForce RTX 4060
VRAM 8GB
OS Windows11
CUDA ver.13.3
LLMサーバー llama.cpp-server

llama.cpp-serverは、 GitHubの ggml-org /llama.cpp にある、ReleaseからWindows用バイナリー(CUDA対応)をダウンロードして利用しました。
LLMモデルは、huggingfaceのQwen3-Coder-30B-A3B-Instruct-GGUFから、4bit量子化板のQwen3-Coder-30B-A3B-Instruct-Q4_K_M.ggufをダウンロードしました。ファイルサイズは、18.6GBです。VRAMの8GBよりもかなり大きいので普通ならば実行できません。たとえ、メインRAMを食って実行できたとしても遅くて使い物になりません。

Qwen3-Coder-30B-A3B-Instruct-Q4_K_M.ggufについて

  • 30Bは約300億個のパラメータ
  • A3B(Active 3B)は推論時に毎回の計算でアクティブなパラメータが約30億
  • Q4_K_Mは量子化手法

です。

llama.cpp-serverは、huggingfaceからダウンロードしたファイルをそのまま使えます。
また、ブラウザでアクセスできるGUIを持っていて、スクリプトを書かなくても利用できます。

実行パラメータ

llama-server.exeを動かすためのバッチファイルです。
パラメータ指定で、アクティブな部分のみをVRAMに、その他はメインRAMに割り当てることで、VRAMの容量を超えるモデルを実行します。

start-llama.bat
E:\llama\llama-server ^
  -m "E:\llama\models\Qwen3-Coder-30B-A3B-Instruct-Q4_K_M.gguf" ^
  --port 9000 ^
  -c 12288 ^
  --temp 0.1 ^
  --top-p 0.9 ^
  -ngl 99 --cpu-moe ^
  --repeat-penalty 1.1 

バッチファイルの中で重要な部分は、CPUオフロードを使うための

start-llama.bat
  -ngl 99 --cpu-moe

です。アクティブでない部分をメインメモリに移します。
VRAMに入っている部分はGPUが処理、メインメモリに入っている部分はCPUが処理するようになります。
-ngl 99 だけですと、すべてをVRAMにロードしようとします。VRAMに入り切らない部分は共有GPUメモリ(メインRAM)に移されますが、すべてのパラメータを一度に使おうとしますので、推論が極端に遅くなります。

テストプロンプト

ブラウザから http://localhost:9000/ (上記設定の --port 9000 によります)にアクセスすると、よくある画面が出てきます。
今回はテストとして、ブラウザで動く ライフゲーム を作ってもらいます。

ライフゲームとは
単純なルールで、生命の誕生や死を再現する数理モデルです。
むかし、流行りました。

テストプロンプト
HTMLとJavaScriptを使用して、ブラウザで動くコンウェイのライフゲーム(Conway's Game of Life)のプログラムを作成してください。
画面にはクリックで細胞(セル)の生死を切り替えられるグリッド、[スタート]、[ストップ]、[リセット]、[1ステップ進める]のボタンを配置してください。
デザインはシンプルで見やすくしてください。
グリッド線を入れてください。
初期値やリセット値は、すべて空にしてください。
生きているセルを黒としてください。
JavaScript、CSSスタイルは、HTMLファイルとは別にして下さい。
HTMLファイルはlife.html、JavaScriptはlife.js、CSSはlife.cssという名前で出力して下さい。

結果

Qwen3-Coderは動くコードを出力してくれました。しかも実用的な速さでした。
LLMをロードする前

VRAMは空、メインRAMは25GBほどの余裕があります。
コード作成中
GPUは100%、VRAMは3.3GBです。

メインメモリーは17.9GB(=24.0-6.1)ほど消費しています。

CPUは78%ほど稼働しています。

そして、26.69token/sec出ました。十分な速さです。

出来たソースコードです。

life.html
life.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <title>Conway's Game of Life</title>
  <link rel="stylesheet" href="life.css" />
</head>
<body>
  <h1>Conway's Game of Life</h1>
  <div class="controls">
    <button id="startBtn">スタート</button>
    <button id="stopBtn">ストップ</button>
    <button id="resetBtn">リセット</button>
    <button id="stepBtn">1ステップ進める</button>
  </div>
  <canvas id="gridCanvas" width="600" height="400"></canvas>

  <script src="life.js"></script>
</body>
</html>
life.css
life.css
body {
  font-family: Arial, sans-serif;
  text-align: center;
  background-color: #f5f5f5;
}

h1 {
  color: #333;
}

.controls {
  margin-bottom: 20px;
}

button {
  padding: 10px 15px;
  margin: 5px;
  font-size: 16px;
  cursor: pointer;
  background-color: #4CAF50;
  color: white;
  border: none;
  border-radius: 4px;
}

button:hover {
  background-color: #45a049;
}

#gridCanvas {
  border: 2px solid #333;
  background-color: white;
}
life.js
life.js
const canvas = document.getElementById('gridCanvas');
const ctx = canvas.getContext('2d');

const CELL_SIZE = 10;
const COLS = Math.floor(canvas.width / CELL_SIZE);
const ROWS = Math.floor(canvas.height / CELL_SIZE);

let grid = createEmptyGrid();
let nextGrid = createEmptyGrid();
let running = false;
let intervalId = null;

function createEmptyGrid() {
  return Array(ROWS).fill().map(() => Array(COLS).fill(false));
}

function drawGrid() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // グリッド線を描画
  ctx.strokeStyle = '#ccc';
  ctx.lineWidth = 0.5;
  for (let y = 0; y <= ROWS; y++) {
    ctx.beginPath();
    ctx.moveTo(0, y * CELL_SIZE);
    ctx.lineTo(canvas.width, y * CELL_SIZE);
    ctx.stroke();
  }
  for (let x = 0; x <= COLS; x++) {
    ctx.beginPath();
    ctx.moveTo(x * CELL_SIZE, 0);
    ctx.lineTo(x * CELL_SIZE, canvas.height);
    ctx.stroke();
  }

  // 生きているセルを描画
  ctx.fillStyle = 'black';
  for (let y = 0; y < ROWS; y++) {
    for (let x = 0; x < COLS; x++) {
      if (grid[y][x]) {
        ctx.fillRect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE);
      }
    }
  }
}

function countNeighbors(x, y) {
  let count = 0;
  for (let dy = -1; dy <= 1; dy++) {
    for (let dx = -1; dx <= 1; dx++) {
      if (dx === 0 && dy === 0) continue;
      const nx = x + dx;
      const ny = y + dy;
      if (nx >= 0 && nx < COLS && ny >= 0 && ny < ROWS) {
        if (grid[ny][nx]) count++;
      }
    }
  }
  return count;
}

function updateGrid() {
  for (let y = 0; y < ROWS; y++) {
    for (let x = 0; x < COLS; x++) {
      const neighbors = countNeighbors(x, y);
      if (grid[y][x]) {
        nextGrid[y][x] = neighbors === 2 || neighbors === 3;
      } else {
        nextGrid[y][x] = neighbors === 3;
      }
    }
  }

  // グリッドを更新
  for (let y = 0; y < ROWS; y++) {
    for (let x = 0; x < COLS; x++) {
      grid[y][x] = nextGrid[y][x];
    }
  }
}

function toggleCell(x, y) {
  const col = Math.floor(x / CELL_SIZE);
  const row = Math.floor(y / CELL_SIZE);
  if (col >= 0 && col < COLS && row >= 0 && row < ROWS) {
    grid[row][col] = !grid[row][col];
    drawGrid();
  }
}

canvas.addEventListener('click', function(e) {
  const rect = canvas.getBoundingClientRect();
  const x = e.clientX - rect.left;
  const y = e.clientY - rect.top;
  toggleCell(x, y);
});

document.getElementById('startBtn').addEventListener('click', () => {
  if (!running) {
    running = true;
    intervalId = setInterval(() => {
      updateGrid();
      drawGrid();
    }, 100);
  }
});

document.getElementById('stopBtn').addEventListener('click', () => {
  running = false;
  clearInterval(intervalId);
});

document.getElementById('resetBtn').addEventListener('click', () => {
  running = false;
  clearInterval(intervalId);
  grid = createEmptyGrid();
  drawGrid();
});

document.getElementById('stepBtn').addEventListener('click', () => {
  updateGrid();
  drawGrid();
});

drawGrid(); // 初期描画
プログラムもちゃんと動きました。

「グライダー」が飛んでゆきます。
JavaScriptは、プロンプトを新規実行する毎に変化しますが、HTMLやCSSはほとんど変わりません。
また、プロンプトの僅かな修正で結果が変わってしまいました。

他のパラメータ

-ngl 99 --cpu-moe 以外のパラメータも試してみました。

設定 token/sec
-ngl 99 5.0
-ngl 99 --flash-attn on
--cache-type-k q8_0 --cache-type-v q8_0
5.1
-ngl 99 --n-cpu-moe 32 --flash-attn on 12.9
-ngl 99 --n-cpu-moe 128 --flash-attn on 27.0
-ngl 99 --cpu-moe 27.0
-ngl auto --flash-attn on 28.9

MoEモデルはすごい!

MoE(Mixture of Experts)(萌え)モデルのすばらしさを実感しました。
VRAMには余裕がありますので、メインメモリーを増やせば、もっと大きなモデルでも動かすことができる?かも

2
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
2
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?