【ローカルAI】中古ワークステーションで生成AIを始める(第1回)
はじめに(導入)
最近では、Claude CodeやGitHub Copilotなど、AIを利用したプログラム開発支援ツールが注目されています。
これらのサービスを利用すると、コード生成や修正を効率化できます。しかし、多くのオンラインAIサービスでは、契約プランや利用量による制限があります。
特に、AIに何度も質問したり大きなプログラムを解析させたりすると、利用可能な回数や利用量が契約の上限に達してしまい、利用量を意識する場面があります。そこで、自分のPC上で動作するローカル生成AIは使えるのかと思い立ち調べてみることにしました。世の中には記事がいっぱいありますが、まずは自分で体験してみてどうかです。Mac のM4チップを搭載し、メモリもマシマシに積めるPCを持ってない人向けの記事を書こうと思います。
つまり、業務用で使われてた昔のワークステーションやサーバがオークションでごろごろころがってます。またゲーミングPCのGPUを買い替えて古いほうが余ってる場合や同居する場合も考えられます。
今余っているGPUがAMDのものですが、ノートPCにはNVIDIAのRTX A2000 Laptopがのってるので比較もできると思います。
使用するPC環境
今回、ローカル生成AIを動かすために使用するPCは、Lenovo ThinkStation P710です。
今回使用する構成は以下になります。
ThinkStationシリーズは、企業や研究機関などで利用されるワークステーションです。一般的なデスクトップPCとは違い、メモリ容量や拡張スロットなどが充実している点が特徴です。本体は中古で入手しました。本体だけで2万円ぐらいでした。CPUは1つついてましたが、20コア(40スレッド)に変更し、CPUを2個搭載するため、同型のCPUをAli Expressで購入。さらにメモリ増設をしました。メモリは一時期安かった時期があったのでECC付きのメモリーをつけてます。メモリスロットは12本あります。


内部構造は、上にCPUが2個。トータルで192GBのメモリ。PCI Express スロットが中段にあり6個利用可能です。筐体が大きいので大きなグラボでも余裕があります。1slot厚なら6枚まで。GPUなら3枚まで搭載が可能とP710のユーザーマニュアルに書いてあります。GPUのスロット厚があると、隣のPCI Expressスロットを埋めてしまうので、搭載枚数はその分少なくなります。電源容量は750Wです。電源は専用電源がついています。4つの3.5インチストレージベイがあります。
GPUは最新のAI向けGPUを購入したものではなく、以前使用していたPCから取り外したRX6750 XTを再利用しています。
また、ノートPCのThinkPad P1 (NVIDIA RTX A2000 Laptop 搭載)や ThinkStation P330 Tiny(Quadro P620搭載)もあるので、比較していきたいと思います。
LM Studioについて
LM Studioは、ローカルでLLMを動かすアプリであり、GUIでモデル管理ができます。モデルはGGUF形式のものが利用でき、Windows/Linux/macOSで利用できます。
セットアップ
今回は余ってるGPUがAMDのなので、OSはDebian 13を入れます。Ubuntuでもいいです。Debianを入れたのはNASに入れてあるのがDebianだったのとproxmoxも使っているからです。AMDだとwindowsではパフォーマンスが出ないかもしれないというのを聞いたので、Windows はNvME のストレージに入れたまま、Linuxを余ってたSSDに入れてデュアルブートとします。
入れるソフトウェア構成は、デスクトップ環境としてxfceとgnome。sshサーバーを指定してリモートでつなげることができるようにしておきます。sshを入れるとTeratermから遠隔で入れます。入れたあとwindowsからリモートデスクトップで接続できるように設定します。OSセットアップ途中でLM Studioを使うユーザーとして、lm_studio ユーザを作りました。
インストール後にログインしたところです。

Teraterm が手持ちのPCにあるので、普段Teratermとwindowsのリモートデスクトップから入れるようにします。
Teratermでログインして、以下のソフトウェアをいれます。
Teratermでログイン後にxrdpを入れます。
sudo apt update
sudo apt install xrdp
xrdpのサービスが起動しているかを確認します。Active欄に activeと出てれば良いです。
systemctl status xrdp
xrdpではログイン時に起動するデスクトップを指定できます。xfceのほうが軽量なのでおすすめです。OSインストール時に入れてなかったら入れます。
sudo apt install xfce4 xfce4-goodies
lm studioユーザーで、X Windowのセッション情報をxfceを使うように設定します。lm studioユーザーで以下を実施します。
echo "startxfce4" > ~/.xsession
Linux側でGUIでログインしていたらlm studioユーザはログアウトします。そのあとwindows上からリモートデスクトップでLinuxに接続します。

GPUが認識されているか確認する
まず、LinuxがGPUそのものを認識しているか確認します。TeratermかLinux上のターミナルからコマンドを入れます。
lspci | grep -i vga
lspci は認識している pci expressのカードがあれば表示されるので、GPUとして認識しているかを見ます。
「lspci -k | grep amd」を使うとGPUがAMDの場合は表示されます。nvidia なら amd を nvidia にします。

この時点ではGPUがLinuxから認識されてドライバーが動作していることを確認します。AIアプリケーションでGPUを利用できるかは、LM Studio導入後に確認します。
LM Studioのインストール
LM Studioは、PC上で大規模言語モデル(LLM)を動作させるためのアプリケーションです。モデルの検索やダウンロード、チャット形式での実行をGUIから行うことができます。「https://lmstudio.ai」から各OS用のLM Studioがダウンロードできます。Linux上からサイトにアクセスしたので、Linux版のダウンロード画面が出ています。

ダウンロードしたファイルに実行権限をつけて、ターミナルから起動します。
chmod 755 LM-Studio-0.4.20-1-x64.AppImage
./LM-Studio-0.4.20-1-x64.AppImage

起動しました。
小さいLLMモデルを導入する
左に縦に並んでるアイコンにModel Searchがあります。クリックするとモデルの検索画面が出てきます。
画面の4Bは、そのAIモデルが持っているパラメータ数の大きさを表しており、4B = 約40億個のパラメータの意味です。4Bよりも8B、8Bよりも14Bが持ってるパラメーターは多くなります。初回ではQwen3 4B Instruct を入れてみます。

Downloadボタンを押してダウンロードが終わると、ボタンが「Use in New Chat」になるので、ボタンを押します。

画面がチャット画面になりますので、日本語で入力してみます。
文章が出てくる速度は亀のように遅いです。ずらずらと表示されてくる程度です。LLMがGPUを使ってるかを見てみることにします。

AMDなので「radeontop」というコマンドを実行してみます。VRAMの使用量は300Mほどで、全くLLMで使用されてませんでした。

たぶんLM Studioでは、GPUは使われずにCPU推論で動いてます。
またvulkaninfoで、使用しているGPUが出てくるかを見ます。自宅のPCではrootで実行するか、ユーザーかで違いました。GPU0 / GPU1 が出るか、GPU0だけかの違いはありました。
vulkaninfo --summary

ユーザー権限を確認します。
groups
lm_studio users という表示だけなので、renderグループを追加します。以下を実行しました。
sudo usermod -aG render,video lm_studio
ログアウトして再ログインして groups を確認します。
lm_studio users render が表示されました。
次は LM Studio側の設定を確認します。設定からRuntimeを表示して、Runtime Selectionsを見ると、一覧には CPU llama.cpp (linux) や vulkan llama.cpp (linux)があります。CPUになってるので vulkan にします。そのあと、モデルの横の設定から、GPUオフロードの値を0からMaxに変更します。


これで動かしてみます。
1から100までの数字で、素数を表示するプログラムを、python3、php、ruby、java、c、c++、rust、juliaで作ってください。
と注文をだしました。生成はできました。速度はオンラインのと比べると遅いですが実用レベルです。

python3
def is_prime(n):
if n < 2:
return False
if n == 2:
return True
if n % 2 == 0:
return False
for i in range(3, int(n**0.5) + 1, 2):
if n % i == 0:
return False
return True
primes = [n for n in range(1, 101) if is_prime(n)]
print("1から100までの素数:")
print(primes)
python3 のコードは動きました。
php
<?php
function is_prime($n) {
if ($n < 2) return false;
if ($n == 2) return true;
if ($n % 2 == 0) return false;
for ($i = 3; $i * $i <= $n; $i += 2) {
if ($n % $i == 0) return false;
}
return true;
}
$primes = [];
for ($num = 1; $num <= 100; $num++) {
if (is_prime($num)) {
$primes[] = $num;
}
}
echo "1から100までの素数:\n";
print_r($primes);
?>
phpのコードも動きました。
ruby
def is_prime(n)
return false if n < 2
return true if n == 2
return false if n.even?
(3..Math.sqrt(n)).step(2) do |i|
return false if n % i == 0
end
true
end
primes = (1..100).select { |n| is_prime(n) }
puts "1から100までの素数:"
puts primes
ruby のコードも動きました
java
import java.util.*;
public class PrimeNumbers {
public static boolean isPrime(int n) {
if (n < 2) return false;
if (n == 2) return true;
if (n % 2 == 0) return false;
for (int i = 3; i * i <= n; i += 2) {
if (n % i == 0) return false;
}
return true;
}
public static void main(String[] args) {
List<Integer> primes = new ArrayList<>();
for (int num = 1; num <= 100; num++) {
if (isPrime(num)) {
primes.add(num);
}
}
System.out.println("1から100までの素数:");
System.out.println(primes);
}
}
C
#include <stdio.h>
#include <math.h>
int is_prime(int n) {
if (n < 2) return 0;
if (n == 2) return 1;
if (n % 2 == 0) return 0;
for (int i = 3; i * i <= n; i += 2) {
if (n % i == 0) return 0;
}
return 1;
}
int main() {
printf("1から100までの素数:\n");
for (int num = 1; num <= 100; num++) {
if (is_prime(num)) {
printf("%d ", num);
}
}
printf("\n");
return 0;
}
C言語のコードも動きました
C++
#include <iostream>
#include <cmath>
#include <vector>
using namespace std;
bool is_prime(int n) {
if (n < 2) return false;
if (n == 2) return true;
if (n % 2 == 0) return false;
for (int i = 3; i * i <= n; i += 2) {
if (n % i == 0) return false;
}
return true;
}
int main() {
vector<int> primes;
for (int num = 1; num <= 100; num++) {
if (is_prime(num)) {
primes.push_back(num);
}
}
cout << "1から100までの素数:" << endl;
for (int p : primes) {
cout << p << " ";
}
cout << endl;
return 0;
}
C++言語のソースコードも動きました
rust
fn is_prime(n: u32) -> bool {
if n < 2 { return false; }
if n == 2 { return true; }
if n % 2 == 0 { return false; }
for i in (3..=n as f64.sqrt() as u32).step_by(2) {
if n % i == 0 {
return false;
}
}
true
}
fn main() {
let primes: Vec<u32> = (1..=100)
.filter(|&n| is_prime(n))
.collect();
println!("1から100までの素数:");
println!("{:?}", primes);
}
rust のコードは動きませんでした。以下が間違ってました。
3..=n as f64.sqrt()
→ 3..=(n as f64).sqrt()
n as f64 と sqrt()の優先順位があいまいで、優先順位をつけるために() を追加して修正したら動きました。
julia
function is_prime(n::Int)
if n < 2
return false
end
if n == 2
return true
end
if n % 2 == 0
return false
end
for i in 3:floor(Int, sqrt(n)):2
if n % i == 0
return false
end
end
return true
end
primes = [n for n in 1:100 if is_prime(n)]
println("1から100までの素数:")
println(primes)
julia> println(primes) [2, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97, 99]
素数を表示なのに、素数になってませんでした。間違ってることをLLMに伝えます。
function is_prime(n)
if n < 2
return false
end
if n == 2
return true
end
if n % 2 == 0
return false
end
# 奇数だけをチェック(3, 5, 7, ...)
i = 3
while i * i <= n
if n % i == 0
return false
end
i += 2
end
return true
end
# 1から100までの素数をリストに格納
primes = []
for n in 1:100
if is_prime(n)
push!(primes, n)
end
end
# 結果を表示
println("1から100までの素数:")
println(join(primes, ", "))
今度は素数は正しく表示されました。
julia> println(join(primes, ", "))
2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97
ローカル生成AIでは、メジャーなプログラミング言語では生成されたプログラムは合ってるのではと思いますが、ものにより間違いがあるかもしれません。
rust や julia は後発のプログラミング言語なので学習が足りないのかもしれません。
今回は1回目なのでこれで検証を終えます。



ディスカッション
コメント一覧
まだ、コメントがありません