【第4回】llama.cppによるCPU/低VRAM環境でのモデル推論:AlmaLinuxで限界を引き出す職人技

ローカル環境におけるLLM(大規模言語モデル)の運用において、最も頭を悩ませる問題は「ハードウェアリソースの枯渇」、とりわけ「GPUのVRAM(ビデオメモリ)容量の不足」です。エンタープライズ向けの高性能GPU(NVIDIA A100やH100など)は数百万〜数千万円と極めて高額であり、すべてのプロジェクトやオンプレミスサーバーに潤沢なGPUリソースを用意できるわけではありません。また、エッジ環境(店舗の小型サーバーや工場のゲートウェイ機器)においては、そもそもGPUを搭載すること自体が不可能なケースも多々あります。

前回の第3回では、Ollamaを利用したモダンなコンテナAI基盤の構築を解説しました。Ollamaは非常に便利ですが、裏側ではプロセスを自動制御しているため、ハードウェアリソースが極端に制限された環境で「1バイトのメモリ、1Hzのクロックサイクルも無駄にできない」ような限界ギリギリのチューニングを行うには、やや抽象度が高すぎます。

コウ君

リナックス先生、社内の検証環境に余っている古い1Uサーバー(GPU非搭載・CPUのみ)を活用して、社内文書検索用のローカルLLMを動かせないか相談されました。さすがにGPUなしのCPU単体じゃ、LLMなんて動かないですよね……?

リナックス先生

いいえ、コウ君。インフラエンジニアの「職人技」を駆使すれば、CPU単体のサーバーや、VRAMが数GBしかない安価なグラフィックボードの環境でも、十分に実用的な速度でLLMを稼働させることができます。そのための最強の武器が、Ollamaの心臓部でもある推論エンジン「llama.cpp」をネイティブにビルドして直接コントロールする技術です。今回はPythonやコンテナの層を全て取り払い、C/C++レベルのベアメタルな世界で究極の最適化を学んでいきましょう。

本連載「LinuxエンジニアのためのローカルLLM」の第4回となる本記事では、AlmaLinux 9/10環境上で「llama.cpp」をソースコードからコンパイルし、量子化フォーマット「GGUF」の深いメカニズムの理解から、CPUのNUMAアーキテクチャを考慮したスレッドチューニング、そしてVRAMとシステムメモリ(RAM)を協調させるGPUハイブリッド・オフロード機能まで、プロのインフラエンジニアに向けたディープな技術を徹底的に解説します。

📚 本連載のカリキュラム(全8回)


1. なぜ今、llama.cppを直接扱うのか?(アーキテクチャの真髄)

LLMの開発や検証において、データサイエンティストやAIリサーチャーは通常「PyTorch」や「Hugging Face Transformers」といったPythonベースの巨大なエコシステムを使用します。これらのツールは実験や学習には最適ですが、インフラ基盤上で「推論(Inference)だけ」を高速かつリソース効率よく実行するという目的においては、Pythonのオーバーヘッドや肥大化した依存パッケージ(数GBに及ぶライブラリ群)が大きな足かせとなります。

1.1. C/C++ネイティブによる究極の軽量化

「llama.cpp」は、ブルガリアの開発者Georgi Gerganov氏を中心にオープンソースで開発されている推論エンジンです。その最大の特長は「一切の外部依存(PythonやPyTorchなど)を持たず、純粋なC/C++でスクラッチから書かれている」という点にあります。

これにより、以下のようなインフラ視点での圧倒的なメリットが生まれます。

  • ゼロ・オーバーヘッド: OSのシステムコールやメモリ確保を直接行うため、メモリのフットプリント(プロセス自体が消費するRAMの量)が数MB〜数十MBに収まります。
  • バイナリ単体でのポータビリティ: ビルドされた実行ファイル(バイナリ)さえあれば、依存ライブラリをインストールすることなく、どのLinuxサーバーに持っていっても即座に動きます。
  • 幅広いハードウェア対応: Intel/AMDのCPU拡張命令(AVX2, AVX-512)、Apple Silicon(MacのM1/M2)、そしてNVIDIA(CUDA)やAMD(ROCm)のGPUまで、コンパイル時のフラグ一つであらゆるハードウェアアーキテクチャに最適化されます。

1.2. エッジコンピューティングと「Memory-bound」の概念

コウ君

でも、CPUってGPUに比べたら並列計算の能力が圧倒的に低いですよね? いくらC++で速く書かれていても、LLMの膨大な計算をCPUで処理したら、ものすごく遅くなるんじゃないですか?

リナックス先生

実はそこが生成AI(LLM推論)の最大の誤解なんです。LLMのテキスト生成プロセス(Decodingフェーズ)は、計算機の処理能力(Compute-bound)ではなく、「メモリの転送速度(Memory-bound)」に律速されます。つまり、CPUの計算が遅いから推論が遅いのではなく、RAMからCPUへデータを運ぶ「道路の太さ(帯域幅)」が足りないから遅いのです。

LLMが1つのトークン(単語の断片)を生成する際、モデル全体のパラメータ(重みデータ)をストレージではなくメモリからプロセッサ(CPU/GPU)へと一度全て読み込む必要があります。例えば80億(8B)パラメータのモデルの場合、1トークン生成するごとに数GBのデータをメモリから転送しなければなりません。

サーバー用のDDR4/DDR5システムメモリの帯域幅は、チャネル数にもよりますが概ね 50GB/s 〜 200GB/s 程度です。一方、NVIDIAの高価格帯GPUに搭載されているHBM(High Bandwidth Memory)やGDDR6Xメモリの帯域幅は 500GB/s 〜 2000GB/s(2TB/s) に達します。
この「メモリ転送速度の差」こそが、CPUとGPUの推論速度の差の正体です。したがって、CPU環境でLLMを高速に動かすためには、「計算を速くする」ことよりも「転送するデータ量そのものを極限まで圧縮する」アプローチが必要になります。それが次に解説する「量子化」と「GGUF」の技術です。

2. GGUFフォーマットと量子化の深いメカニズム

llama.cppは、モデルのデータサイズを劇的に圧縮するために独自のファイルフォーマット「GGUF(GPT-Generated Unified Format)」を採用しています。Ollamaの裏側で動いているモデルファイルも、実はすべてこのGGUFフォーマットです。

2.1. 重み(Weights)の量子化(Quantization)とは?

AIモデルの「賢さ」は、ニューラルネットワークの各ノード間をつなぐ「重み(Weights)」と呼ばれる数値データに宿っています。学習直後のオリジナルモデルは、この重みを16ビット浮動小数点(FP16/BF16)で保持しています。1パラメータあたり2バイト(16ビット)を消費するため、8Bのモデルであれば最低でも約16GBのファイルサイズ(およびメモリ消費量)となります。

「量子化」とは、この16ビットの数値を、AIの推論精度を可能な限り落とさずに、より少ないビット数(8ビット、4ビット、3ビットなど)の整数(INT)に近似・丸め込む圧縮技術です。例えば 4ビット量子化(4-bit Quantization) を行えば、1パラメータあたりのデータ量は0.5バイトとなり、ファイルサイズとメモリ転送量を一気に 4分の1(約4GB) にまで削減できます。

転送するデータ量が4分の1になれば、メモリ帯域幅(Memory Bandwidth)の狭いCPUのDDRメモリ環境でも、ボトルネックが解消されて高速にトークンを生成(推論)できるようになります。

2.2. k-quants(高度な混合量子化技術)

llama.cppのGGUFフォーマットでは、「k-quants」と呼ばれる高度な量子化メソッドが用いられています。単に全てのパラメーターを一律に4ビットにするのではなく、「推論精度に大きく影響する重要なレイヤーは5ビットや6ビットで保持し、影響の少ないレイヤーは3ビットや4ビットに圧縮する」といった混合精度量子化を行います。

GGUFファイルの名前には、量子化の手法を示すサフィックス(接尾辞)が付与されています。インフラエンジニアとしては、以下の代表的なフォーマットの違いと、リソースに応じた選び方を覚えておく必要があります。

  • Q4_K_M (Medium): 最もバランスの取れた標準的なフォーマット。オリジナル(FP16)と比べて体感できるほどの精度の劣化がなく、サイズは4分の1以下。迷ったらこれを選びます。
  • Q5_K_M: 5ビット中心の量子化。Q4よりも少しサイズは大きくなりますが、より複雑なコーディングタスクや論理推論において精度の向上が見られます。リソースに余裕があれば推奨。
  • Q2_K / Q3_K_S: 極限までサイズを削ったフォーマット。精度(ハルシネーションの少なさ)の低下が顕著に表れるため、エッジデバイスやラズベリーパイなどで「どうしても動かしたい」場合の最終手段です。
  • Q8_0: 8ビット量子化。ほぼオリジナルのFP16と遜色ない精度ですが、サイズは半減にとどまります。VRAMに余裕がある環境向けです。

⚠️ CPUのSIMD命令セットと量子化の相性
インテルやAMDの最近のCPUは、AVX2やAVX-512といった「SIMD(Single Instruction, Multiple Data)」拡張命令セットを搭載しています。llama.cppはこれらの拡張命令に高度に最適化されており、Q4_K_Mなどの特定の量子化フォーマットの計算を、CPUのベクトルレジスタ上で極めて効率的に(GPUに近い並列度で)処理します。これがCPU環境でも信じられないほどの速度を叩き出せる理由です。

3. AlmaLinuxでのllama.cppのネイティブビルドと最適化

アーキテクチャの理論を理解したところで、実際にAlmaLinux 9上で「llama.cpp」をソースコードからビルド(コンパイル)します。既存のバイナリをダウンロードするのではなく、自環境でビルドすることで、稼働しているサーバーのCPU命令セット(AVX2等)やGPU環境(CUDA)に100%最適化された最強の推論エンジンを生み出すことができます。

3.1. 必須開発ツールの導入

ソースコードの取得とコンパイルに必要なパッケージ群(GCC, CMake, Gitなど)を dnf でインストールします。

# パッケージの最新化とEPEL/CRBリポジトリの有効化(第2回で設定済みの場合はスキップ可能)
sudo dnf update -y
sudo dnf install -y epel-release
sudo dnf config-manager --set-enabled crb

# C/C++コンパイラとビルドツールのインストール
sudo dnf install -y git gcc gcc-c++ make cmake

3.2. ソースコードの取得とCPUオンリー(OpenBLAS)ビルド

まずは、GPUが全く搭載されていない「CPU単体のサーバー」を想定したビルド手順です。行列計算ライブラリとして最適化されたOpenBLASを利用してコンパイルします。

# OpenBLAS開発パッケージのインストール
sudo dnf install -y openblas-devel

# llama.cppのリポジトリをクローン
cd ~
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp

# Makeコマンドを用いて、OpenBLASを有効化してビルド
make LLAMA_OPENBLAS=1 -j $(nproc)

※ -j $(nproc) は、サーバーに搭載されている全てのCPUコアを並列で使用してコンパイルを高速化するオプションです。

3.3. NVIDIA GPU対応(CUDA)バックエンドでのビルド

もしサーバーに少しでもNVIDIAのGPU(RTX 3060やTesla T4など、VRAMが少ない安価なGPUでも構いません)が搭載されており、第2回の記事で解説したCUDA Toolkitがインストールされている場合は、必ずCUDA対応でビルドしてください。

llama.cppの真骨頂は、モデルの一部をGPUで計算し、VRAMに乗り切らなかった残りの部分をCPUで計算する「ハイブリッド・オフロード処理」にあります。

# 既存のビルドキャッシュをクリア
make clean

# CUDAバックエンドを有効化してビルド(第2回のCUDA環境構築が必須)
make LLAMA_CUDA=1 -j $(nproc)

ビルドが成功すると、ディレクトリ内に llama-cli(旧名称:main)や llama-server(旧名称:server)といった実行バイナリが生成されます。

コウ君

おお、本当に依存関係なしでバイナリファイルが出来上がりました。これならコンテナすら使わずに、直接systemdでサービス化することも簡単ですね。

3.4. Hugging Face CLIを用いたGGUFモデルのダウンロード

次に、AIコミュニティの世界最大のプラットフォームである「Hugging Face」から、量子化済みのGGUFモデルをダウンロードします。
Pythonの huggingface_hub パッケージに含まれるCLIツールを利用するのが最も安全で確実です(依存を避けるため、ここではPythonの仮想環境 venv を使用します)。

# Python仮想環境の作成と有効化
python3 -m venv ~/hf-env
source ~/hf-env/bin/activate

# huggingface_hubのインストール
pip install huggingface_hub

# モデルを保存するディレクトリの作成
mkdir -p ~/models

# Llama-3-8B-Instructの「Q4_K_M」量子化GGUFファイルをダウンロード
huggingface-cli download \
  QuantFactory/Meta-Llama-3-8B-Instruct-GGUF \
  Meta-Llama-3-8B-Instruct.Q4_K_M.gguf \
  --local-dir ~/models \
  --local-dir-use-symlinks False

# 仮想環境から抜ける
deactivate

4. CPU/低VRAM環境での高度なチューニングとAPI実稼働

バイナリとモデルが揃いました。ここからはインフラエンジニアとしての真価が問われる「ハードウェア・リソースの限界を引き出すチューニング実行」を行います。

4.1. VRAMとRAMの分割処理:GPUオフロード(-ngl)の魔法

モデルをVRAMに全て読み込もうとして CUDA Out of Memory になるのを防ぐため、llama.cppには -ngl(Number of GPU Layers)という強力なオプションが存在します。これは「LLMのニューラルネットワーク層(Layer)のうち、下から何層までをGPU(VRAM)にロードし、残りをCPU(システムRAM)に任せるか」を指定するものです。

例えば、Llama 3 8Bモデルは約33個のレイヤーを持っています。VRAMが4GBしかない古いGPUサーバーの場合、全層(33層)をVRAMに乗せることはできません。

# GPUに15層だけオフロードし、残りはCPUで処理させる実行例(対話型チャット)
./llama-cli \
  -m ~/models/Meta-Llama-3-8B-Instruct.Q4_K_M.gguf \
  -ngl 15 \
  -c 2048 \
  --color -i -ins

-ngl 15 と指定することで、約半分のレイヤーの行列計算を高速なGPUに任せ、残りをCPUで行います。これにより、VRAMをオーバーフローさせることなく、CPU単体よりも数倍〜数十倍高速な推論を実現できます。システム運用時は nvidia-smi コマンドでVRAMの使用量を見ながら、OOMキラーが発動しないギリギリの -ngl 値を探り当てるのがチューニングの基本です。

4.2. CPUスレッド数の最適化とHyper-Threadingの罠(-t)

CPUオフロードを利用する、あるいはCPU単体で推論を行う場合、CPUのスレッド数(-t オプション)の指定がパフォーマンスを大きく左右します。

⚠️ 絶対的な鉄則:スレッド数は「物理コア数」に合わせる
一般的なWebサーバー(Nginxなど)のチューニングでは、論理コア数(Hyper-Threadingで2倍になったスレッド数)に合わせてプロセスを立ち上げますが、LLMの推論では逆効果です。メモリ帯域(Memory Bandwidth)を奪い合うため、HTの論理スレッドを使用するとキャッシュの競合(スラッシング)が発生し、逆に処理速度が低下します。必ず lscpu コマンドで「コア数 (Core(s) per socket)」を確認し、その数以下の物理コア数を -t に指定してください。

# 物理コアが8個のサーバーの場合、スレッド数を8に指定して実行
./llama-cli -m ~/models/... -ngl 0 -t 8 -c 2048 -i -ins

4.3. エンタープライズサーバー向け:NUMAノードのバインディング

データセンター向けのデュアルソケット(CPUが2つ搭載されている)サーバーを使用しているインフラエンジニアであれば、「NUMA(Non-Uniform Memory Access)」アーキテクチャの制約を考慮しなければなりません。
CPU 0 が管理しているメモリスロット上のデータを、CPU 1 が読み出そうとすると、ソケット間のインターコネクト(Intel UPIなど)を経由するため深刻なレイテンシが発生します。

llama.cpp実行時にこれを回避するには、OSの numactl コマンドを用いて、プロセスを特定のNUMAノード(CPUと直結したメモリ)に強制的にバインド(固定)します。

# numactlのインストール
sudo dnf install -y numactl

# NUMAノードのトポロジ確認
numactl --hardware

# NUMAノード0(CPU 0とそれに直結したメモリ)だけに限定して実行
numactl --cpunodebind=0 --membind=0 ./llama-cli -m ~/models/... -t 8 -c 2048 -i -ins

この numactl によるCPUとメモリのハードバインディングを行うだけで、デュアルソケットサーバーにおけるLLMのテキスト生成速度(Tokens per second)が数倍に跳ね上がるケースが多々あります。まさにインフラエンジニアならではの職人技です。

4.4. llama-serverによるOpenAI互換APIの立ち上げ

最適なパラメータ(-ngl, -t, numactl)を見つけ出したら、最終的に llama-cli ではなく llama-server バイナリを使用して、社内向けにHTTP APIサーバーとしてデーモン化します。
llama-server はOpenAI互換のREST API(デフォルトポート:8080)を提供するため、社内システムからの呼び出しはChatGPTと全く同じコードで実装できます。

# llama-serverを用いたAPIエンドポイントの起動例
# ホスト(0.0.0.0)でListenし、物理8スレッド、GPUオフロード15層を指定
numactl --cpunodebind=0 --membind=0 ./llama-server \
  -m ~/models/Meta-Llama-3-8B-Instruct.Q4_K_M.gguf \
  --host 0.0.0.0 \
  --port 8080 \
  -t 8 \
  -ngl 15 \
  -c 4096

本番環境へ導入する際は、第3回の記事で解説した「Quadletによるsystemd化」と「Nginxによるリバースプロキシ(TLS化・Basic認証)」を前段に構築し、セキュアなAPI基盤として完成させます。

5. まとめと次回の予告

本記事「【第4回】llama.cppによるCPU/低VRAM環境でのモデル推論」では、限られたハードウェアリソースを極限まで絞り出すための以下の高度なチューニング手法を解説しました。

  • ゼロ・オーバーヘッドアーキテクチャ: PyTorch等のPythonエコシステムを排除し、C/C++ネイティブの llama.cpp をAlmaLinux上で直接ビルドする手法を確立した。
  • GGUF量子化とメモリ帯域: LLM推論のボトルネックが「計算速度」ではなく「メモリ帯域幅」にあることを理解し、Q4_K_Mなどの量子化フォーマットで転送データを1/4に圧縮する重要性を学んだ。
  • GPUオフロード(-ngl)の魔法: VRAMに収まらないモデルをGPUとCPU(システムRAM)で分割処理し、OOMを回避しながら推論速度を最大化する設計を行った。
  • スレッドとNUMA最適化: Hyper-Threadingを無効化し物理コア数に一致させるスレッド設計(-t)と、numactl を用いたエンタープライズサーバーのレイテンシ削減手法を実践した。

これらの職人技を駆使すれば、高価なGPUを調達できない環境であっても、十分に社内利用が可能なローカルLLM基盤を構築することができます。

しかし、社内の全従業員からアクセスが集中するような「超高トラフィック」な本番チャットボットサービスを想定した場合、Ollamaやllama.cppのアーキテクチャでは対応しきれない「同時並行処理(バッチサイズ)」の壁にぶつかります。
そこで次回、【第5回】vLLMを利用した本番向け高スループット推論サーバーの構築では、最新のメモリ管理技術「PagedAttention」を搭載し、大量の同時リクエストを捌くことに特化したエンタープライズ向け推論エンジン「vLLM」のアーキテクチャと構築手法に迫ります。ご期待ください!

▼ 【インフラ環境を構築しよう】 ▼

リソース制限のシビアな環境構築に!
「高コスパおすすめVPS・専用サーバー」

VPSランキングを見る

オンプレミスの限界に挑む技術者へ
「インフラエンジニア専門転職」

転職エージェントを見る

コメント