はじめに
Microsoft Research AI for Science が開発した NatureLM(Nature Language Model)は、小分子・材料・タンパク質・DNA・RNA を横断する科学基盤モデルです。本記事では、NVIDIA GPU を持たないローカル環境で NatureLM-8x7B-Inst を動かし、Ollama・Lemonade Server・llama-cpp-python・自作 OpenAI 互換 API サーバー の 4 つのアプローチを比較検証しました。
NatureLM とは
- 論文: Nature Language Model: Deciphering the Language of Nature for Scientific Discovery
- リポジトリ: https://github.com/microsoft/SFM
- モデル: Mixtral-8x7B ベースの MoE(Mixture of Experts)アーキテクチャ
- パラメータ数: 46.7B(8 × 7B エキスパート、推論時は 2 エキスパート活性化)
- 語彙サイズ: 38,078トークン(標準 Mixtral の 32,000 に対し、科学ドメイン用の特殊トークン約 6,000 個を追加)
- ライセンス: MIT License
実験環境
| 項目 | スペック |
|---|---|
| OS | Windows 11 |
| CPU | AMD(スレッド数: 全コア使用) |
| GPU | AMD Radeon 8060S(4GB VRAM) |
| RAM | 64 GB |
| Python | 3.13.12 |
| Ollama | 0.20.0 |
| Lemonade Server | 10.0.1 |
| llama-cpp-python | 0.3.19 |
| PyTorch | 2.11.0+cpu |
| transformers | 5.5.0 |
NVIDIA GPU なしのため、vLLM(CUDA 必須)は使用できません。本記事では CPU 推論および AMD GPU(Vulkan)推論で検証しています。
1. Ollama でのセットアップ
1.1 GGUF モデルの取得
NatureLM の公式 HuggingFace リポジトリは safetensors 形式のみ提供しています。Ollama で使用するために、コミュニティが変換した GGUF 版を利用しました。
| GGUF リポジトリ | 量子化バリエーション |
|---|---|
| DevQuasar/microsoft.NatureLM-8x7B-Inst-GGUF | 2-bit 〜 16-bit |
| gabriellarson/NatureLM-8x7B-Inst-GGUF | IQ1_S 〜 F16 |
1.2 インストール
# HuggingFace から直接 pull(Q4_K_M 量子化、約 28GB)
ollama pull hf.co/DevQuasar/microsoft.NatureLM-8x7B-Inst-GGUF:Q4_K_M
ダウンロード完了後、ollama list で確認:
NAME ID SIZE MODIFIED
hf.co/DevQuasar/microsoft.NatureLM-8x7B-Inst-GGUF:Q4_K_M 08f0eafd76d9 28 GB ...
1.3 プロンプトテンプレートの設定
NatureLM は論文中で以下のテンプレートを使用しています:
Instruction: {instruction}
Response:
{response}
Ollama のデフォルトではテンプレートが設定されないため、Modelfile を作成してカスタムモデルを登録しました。
FROM hf.co/DevQuasar/microsoft.NatureLM-8x7B-Inst-GGUF:Q4_K_M
TEMPLATE """Instruction: {{ .Prompt }}
Response:
"""
PARAMETER stop "Instruction:"
PARAMETER stop "</s>"
PARAMETER temperature 0.7
PARAMETER num_predict 512
ollama create naturelm -f Modelfile-NatureLM
1.4 Ollama テスト結果
テスト方法
Ollama REST API(/api/generate)を使用し、raw: true で直接プロンプトを送信。
curl http://localhost:11434/api/generate -d '{
"model": "naturelm",
"prompt": "Instruction: Predict the logP value of the molecule CC(=O)OC1=CC=CC=C1C(=O)O\n\n\nResponse:\n",
"stream": false,
"raw": true,
"options": { "num_predict": 256, "temperature": 0.7 }
}'
結果
| タスク | プロンプト | Ollama 出力 | 判定 |
|---|---|---|---|
| 物性予測(logP) | Predict the logP value of the molecule CC(=O)OC1=CC=CC=C1C(=O)O |
0.89 logP |
✅ |
| 物性予測(分子量) | Predict the molecular weight of the molecule CC(=O)OC1=CC=CC=C1C(=O)O |
268.20 |
✅ |
| 分子生成 | Generate a molecule with four hydrogen bond donors. |
(空) | ❌ |
| SMILES 生成 | What is the SMILES notation for caffeine? |
(空) | ❌ |
| 逆合成 | Perform retrosynthesis on the molecule CC(=O)OC1=CC=CC=C1C(=O)O |
(空) | ❌ |
数値を出力するタスクは成功するが、SMILES・分子構造・タンパク質配列などの科学エンティティを生成するタスクは出力が空になる。
原因分析
NatureLM は標準 Mixtral の語彙(32,000 トークン)に加え、科学ドメイン用の特殊トークン約 6,000 個を追加しています:
-
<mol>,</mol>— 分子 SMILES の開始・終了 -
<m>C,<m>N,<m>=,<m>(… — SMILES 構成要素の個別トークン -
<protein>,</protein>— タンパク質配列 -
<material>,</material>— 材料組成 -
<sg1>〜<sg230>— 結晶空間群
GGUF 変換時にこれらの特殊トークンのテキスト表現が失われるため、Ollama(llama.cpp のデフォルトトークナイザー)ではデコードできず、出力が空文字列となります。
2. Lemonade Server でのセットアップ
Lemonade Server は AMD が支援するローカル AI サーバーで、Vulkan バックエンドによる AMD GPU アクセラレーションと OpenAI 互換 API を提供します。
2.1 利用可能なバックエンド
lemonade recipes
Recipe Backend Status
llamacpp vulkan installed # AMD GPU 推論 ✅
rocm installable # ROCm(要インストール)
cpu installable # CPU フォールバック
2.2 GGUF モデルの登録
Lemonade は独自のモデルレジストリを持ちますが、HuggingFace からカスタム GGUF を pull できます。
方法A: HuggingFace から直接 pull(再ダウンロード)
lemonade-server pull NatureLM-8x7B-Inst \
--checkpoint DevQuasar/microsoft.NatureLM-8x7B-Inst-GGUF:Q4_K_M \
--recipe llamacpp
方法B: Ollama の既存 GGUF を再利用(推奨)
Ollama で既にダウンロード済みの GGUF ファイル(28GB)を再ダウンロードせずに利用できます。
# 1. GGUF ファイルのハードリンクを作成
$ollamaBlob = "$env:USERPROFILE\.ollama\models\blobs\sha256-21191d7e9e0ff928540b94c4539822f1f75ba22fcfbcf82866c230003961c5af"
mkdir C:\Users\$env:USERNAME\lemonade-models
cmd /c mklink /H "C:\Users\$env:USERNAME\lemonade-models\NatureLM-8x7B-Inst-Q4_K_M.gguf" $ollamaBlob
# 2. Lemonade Server を --extra-models-dir 付きで起動
lemonade-server serve --extra-models-dir "C:\Users\$env:USERNAME\lemonade-models"
起動後、lemonade list で確認:
Model Name Downloaded Details
extra.NatureLM-8x7B-Inst-Q4_K_M.gguf Yes llamacpp
--extra-models-dir は .gguf 拡張子のファイルをスキャンします。Ollama のブロブファイルには拡張子がないため、ハードリンク(またはコピー)で .gguf 拡張子を付ける必要があります。ハードリンクならディスク容量を消費しません。
2.3 モデルのロードとテスト
# モデルをロード(Vulkan GPU にオフロード)
lemonade load "extra.NatureLM-8x7B-Inst-Q4_K_M.gguf"
Lemonade は OpenAI 互換 API を提供するため、標準的なクライアントで利用できます。NatureLM は独自のプロンプトテンプレートを必要とするため、Completions API(/v1/completions)で Instruction: / Response: フォーマットを直接指定します。
curl http://localhost:8000/api/v1/completions \
-H "Authorization: Bearer lemonade" \
-H "Content-Type: application/json" \
-d '{
"model": "extra.NatureLM-8x7B-Inst-Q4_K_M.gguf",
"prompt": "Instruction: Predict the logP value of CC(=O)OC1=CC=CC=C1C(=O)O\n\n\nResponse:\n",
"max_tokens": 64,
"temperature": 0.5
}'
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8000/api/v1",
api_key="lemonade"
)
# Completions API でNatureLMテンプレートを直接使用
response = client.completions.create(
model="extra.NatureLM-8x7B-Inst-Q4_K_M.gguf",
prompt="Instruction: Predict the logP value of CC(=O)OC1=CC=CC=C1C(=O)O\n\n\nResponse:\n",
max_tokens=64,
temperature=0.5,
)
print(response.choices[0].text)
2.4 Lemonade テスト結果
| タスク | プロンプト | Lemonade 出力 | 速度 | 判定 |
|---|---|---|---|---|
| 物性予測(logP) | Predict the logP value of CC(=O)OC1=CC=CC=C1C(=O)O |
0.01 logP |
8.9 tokens/s | ✅ |
| 物性予測(分子量) | Predict the molecular weight of CC(=O)OC1=CC=CC=C1C(=O)O |
3.35 |
~9 tokens/s | ✅ |
| SMILES 生成 | What is the SMILES notation for aspirin? |
(空)※24トークン生成 | — | ❌ |
Lemonade も内部的に llama.cpp を使用しているため、Ollama と同じ科学トークンのデコード問題が発生します。数値出力タスクは動作しますが、SMILES・タンパク質配列などの生成タスクは出力が空になります。
ただし、Vulkan バックエンドにより AMD GPU で高速推論(CPU比約3倍)できる点が大きなメリットです。
3. llama-cpp-python + HuggingFace トークナイザー
3.1 アプローチ
推論は llama.cpp(GGUF)で行い、生成されたトークン ID を HuggingFace のオリジナルトークナイザーでデコードするハイブリッド方式を採用しました。
[GGUF モデル] → llm.generate() → トークン ID 列 → HF tokenizer.decode() → テキスト出力
3.2 環境構築
# llama-cpp-python(CPU プリビルド版)
pip install llama-cpp-python \
--prefer-binary \
--extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu
# HuggingFace トークナイザー
pip install huggingface_hub transformers
# PyTorch CPU(トークナイザーの依存)
# ※ Windows Long Path 問題がある場合は短いパスにインストール
pip install torch --index-url https://download.pytorch.org/whl/cpu --target C:\torch_tmp
3.3 実装コード
"""
NatureLM-8x7B-Inst: Interactive runner
Usage: set PYTHONPATH=C:\torch_tmp && python naturelm_test.py
"""
import os
from llama_cpp import Llama
from transformers import AutoTokenizer
GGUF_PATH = os.path.expanduser(
r"~\.ollama\models\blobs\sha256-21191d7e9e0ff928540b94c4539822f1f75ba22fcfbcf82866c230003961c5af"
)
# HuggingFace からオリジナルトークナイザーをロード
tokenizer = AutoTokenizer.from_pretrained(
"microsoft/NatureLM-8x7B-Inst", trust_remote_code=True
)
# GGUF モデルをロード(CPU推論)
llm = Llama(
model_path=GGUF_PATH,
n_ctx=2048,
n_threads=os.cpu_count(),
verbose=False,
)
def generate(instruction: str, max_tokens=512, temperature=0.7) -> str:
"""NatureLM の Instruction-Response テンプレートで生成"""
prompt = f"Instruction: {instruction}\n\n\nResponse:\n"
input_tokens = llm.tokenize(prompt.encode("utf-8"))
gen_tokens = []
eos = llm.token_eos()
for token in llm.generate(input_tokens, temp=temperature):
gen_tokens.append(token)
if token == eos or len(gen_tokens) >= max_tokens:
break
# HF トークナイザーでデコード(科学トークンを正しく復元)
return tokenizer.decode(gen_tokens, skip_special_tokens=False).strip()
# 使用例
result = generate("What is the SMILES notation for caffeine?")
print(result)
# => <mol><m>C<m>n<m>1<m>c<m>(<m>=<m>O<m>)<m>c<m>2<m>c<m>(<m>n<m>c<m>n<m>2<m>C<m>)<m>n<m>(<m>C<m>)<m>c<m>1<m>=<m>O</mol>
3.4 テスト結果
| タスク | プロンプト | llama.cpp デコード | HF トークナイザー デコード | 判定 |
|---|---|---|---|---|
| 分子生成 | Generate a molecule with four hydrogen bond donors. |
(空) | <material><i>Fe<i>Fe...<i>O<i>O...<sg1></material> |
✅ |
| SMILES 生成 | What is the SMILES notation for caffeine? |
(空) | <mol><m>C<m>n<m>1<m>c<m>(<=<m>O<m>)...<m>=<m>O</mol> |
✅🎯 |
| logP 予測 | Predict the logP value of CC(=O)OC1=CC=CC=C1C(=O)O |
0.86 logP |
0.69 logP |
✅ |
カフェインの SMILES 生成結果 Cn1c(=O)c2c(ncn2C)n(C)c1=O は、正しいカフェインの SMILES 表記と一致します。
4. OpenAI 互換 API サーバー(自作)
4.1 動機
3方式の検証から、以下の課題が明らかになりました:
- Ollama / Lemonade:OpenAI 互換 API はあるが、科学トークンをデコードできない
- llama-cpp-python + HF Tokenizer:科学トークンは正しくデコードできるが、API サーバー機能がない
この両方を解決するため、科学トークンを正しくデコードする OpenAI 互換 API サーバーを FastAPI で自作しました。
4.2 アーキテクチャ
クライアント (openai-python, curl, etc.)
│
▼ OpenAI 互換 API
┌──────────────────────────────────────────┐
│ NatureLM API Server (FastAPI) │
│ │
│ ┌───────────┐ ┌───────────────────┐ │
│ │ llama.cpp │───▶│ Token ID 列 │ │
│ │ (GGUF) │ │ [32001, 32045..] │ │
│ └───────────┘ └────────┬──────────┘ │
│ │ │
│ ┌────────▼──────────┐ │
│ │ HF Tokenizer │ │
│ │ (science tokens) │ │
│ └────────┬──────────┘ │
│ │ │
│ ┌────────▼──────────┐ │
│ │ <mol>C=O...</mol> │ │
│ └───────────────────┘ │
└──────────────────────────────────────────┘
4.3 主な機能
| 機能 | 詳細 |
|---|---|
POST /v1/chat/completions |
NatureLM テンプレートを自動適用 |
POST /v1/completions |
生プロンプトで直接制御 |
GET /v1/models |
モデル情報・ケイパビリティ一覧 |
| 科学トークンデコード | HF トークナイザーで <mol>, <protein> 等を正しく復元 |
| ストリーミング |
stream: true で SSE リアルタイム出力 |
| リトライ機能 | 空出力時に自動リトライ(シード変更 + 温度昇温) |
| CORS 対応 | ブラウザアプリから直接利用可 |
4.4 リトライ機能
NatureLM は量子化モデル (Q4_K_M) で一部のプロンプトに対して即座に EOS トークンを出力することがあります。この問題に対処するため、自動リトライ機能を実装しました。
リトライ戦略:
1回目: temperature=0.7, seed=random
2回目: temperature=0.85, seed=random (+0.15)
3回目: temperature=1.0, seed=random (+0.15)
...
最大20回まで(max_retries パラメータで制御)
リトライの仕組み:
- 高レベル API (
llm()) でランダムシードを変えながら生成 - テキスト出力があればそのまま返却
- トークンは生成されたがテキスト化できない場合(科学トークン)→ トークンレベル API + HF デコードにフォールバック
- 全リトライ失敗時は空文字列を返却、
timing.attemptsに試行回数を記録
4.5 インストールと起動
# 依存パッケージ
pip install fastapi uvicorn llama-cpp-python \
--prefer-binary \
--extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu
pip install huggingface_hub transformers
pip install torch --index-url https://download.pytorch.org/whl/cpu --target C:\torch_tmp
# 起動
set PYTHONPATH=C:\torch_tmp
python naturelm_server.py
# => http://localhost:8080
4.6 使用例
Python (openai クライアント)
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8080/v1", api_key="unused")
# SMILES 生成
r = client.chat.completions.create(
model="naturelm-8x7b-inst",
messages=[{"role": "user", "content": "What is the SMILES for caffeine?"}],
)
print(r.choices[0].message.content)
# => <mol><m>C<m>n<m>1<m>c<m>(<m>=<m>O<m>)<m>c<m>2<m>c<m>(<m>n<m>c<m>n<m>2<m>C<m>)<m>n<m>(<m>C<m>)<m>c<m>1<m>=<m>O</mol>
# リトライ付き生成(拡張パラメータ)
r = client.chat.completions.create(
model="naturelm-8x7b-inst",
messages=[{"role": "user", "content": "Generate a SMILES of a kinase inhibitor."}],
extra_body={"max_retries": 10},
)
curl
curl http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"messages": [{"role":"user","content":"What is the SMILES for ibuprofen?"}],
"max_tokens": 256,
"temperature": 0.7,
"max_retries": 5
}'
4.7 テスト結果
| タスク | プロンプト | 結果 | リトライ | 判定 |
|---|---|---|---|---|
| SMILES 生成 | What is the SMILES for caffeine? |
<mol>Cn1c(=O)c2c(ncn2C)n(C)c1=O</mol> |
1回 | ✅🎯 |
| SMILES 生成 | What is the SMILES of ibuprofen? |
<mol>CC(C)Cc1ccc(C(C)C(=O)O)cc1</mol> |
1回 | ✅🎯 |
| 分子設計 | Generate a SMILES of a kinase inhibitor. |
<mol>Cn1cc(-c2cnc3ccc(NC...)cc32)...</mol> |
3回 | ✅ |
| logP 予測 | Predict the logP of CC(=O)OC1=CC=CC=C1C(=O)O |
0.40 logP |
1回 | ✅ |
| ストリーミング | Predict the logP of c1ccccc1 |
-0.41 logP (SSE) |
1回 | ✅ |
キナーゼ阻害剤の生成は最初の2回は EOS で失敗しましたが、3回目のリトライ(temperature=1.0, 新しいシード)で SMILES の生成に成功しました。リトライ機能の有効性を示す好例です。
5. 4方式比較まとめ
| 観点 | Ollama | Lemonade Server | llama-cpp-python + HF | 自作 API Server |
|---|---|---|---|---|
| セットアップ難易度 | ⭐ 簡単 | ⭐⭐ 簡単 | ⭐⭐⭐ やや複雑 | ⭐⭐⭐ やや複雑 |
| 推論バックエンド | llama.cpp (CPU) | llama.cpp (Vulkan GPU 🚀) | llama.cpp (CPU) | llama.cpp (CPU) |
| 推論速度 | ~3 tokens/s | ~9 tokens/s | ~3 tokens/s | ~3 tokens/s |
| 数値出力タスク | ✅ 動作 | ✅ 動作 | ✅ 動作 | ✅ 動作 |
| SMILES 生成 | ❌ 空出力 | ❌ 空出力 | ✅ 正しくデコード | ✅ 正しくデコード |
| タンパク質/材料生成 | ❌ 空出力 | ❌ 空出力 | ✅ 正しくデコード | ✅ 正しくデコード |
| OpenAI 互換 API | ❌ 独自 API | ✅ 標準対応 | ❌ スクリプトのみ | ✅ 標準対応 |
| リトライ機能 | ❌ | ❌ | ❌ | ✅ 自動リトライ |
| ストリーミング | ❌ | ✅ | ❌ | ✅ SSE 対応 |
| AMD GPU 活用 | ❌ | ✅ Vulkan | ❌ | ❌ |
| 既存アプリ連携 | △ | ✅ | ❌ | ✅ OpenAI 互換 |
使い分けガイド
用途に応じて最適な方式を選択:
┌─ 科学トークン + API 連携が必要(推奨)
│ └─→ 自作 API Server(科学トークン + OpenAI互換 + リトライ)
│
├─ 科学トークンが必要(スクリプト利用)
│ └─→ llama-cpp-python + HF Tokenizer
│
├─ 速度重視・AMD GPU(数値タスクのみ)
│ └─→ Lemonade Server(Vulkan GPU加速)
│
└─ とりあえず試したい
└─→ Ollama(最も簡単)
6. 注意事項と制限
カスタムトークナイザーの問題
NatureLM の語彙拡張(38,078 トークン)は GGUF フォーマットのトークナイザーメタデータに完全には反映されません。これは NatureLM 固有の問題ではなく、カスタム語彙を持つモデルを GGUF に変換する際の一般的な課題です。
推奨環境
フル機能でNatureLMを利用するための推奨環境:
| 方式 | GPU 要件 | メモリ要件 | 科学トークン | OpenAI API | 推論速度 |
|---|---|---|---|---|---|
| vLLM + transformers(推奨) | NVIDIA A100 × 2 以上 | 80GB+ VRAM | ✅ 完全対応 | ✅ | 最速 |
| 自作 API Server | 不要(CPU可) | 32GB+ RAM | ✅ 対応 | ✅ | 低速 |
| Lemonade Server (Vulkan) | AMD/NVIDIA GPU | 32GB+ RAM | ⚠️ 数値のみ | ✅ | 高速 |
| llama-cpp-python + HF Tokenizer | 不要(CPU可) | 32GB+ RAM | ✅ 対応 | ❌ | 低速 |
| Ollama(GGUF) | 不要(CPU可) | 32GB+ RAM | ⚠️ 数値のみ | ❌ | 低速 |
量子化モデルでの生成安定性
Q4_K_M 量子化モデルでは、一部の生成タスクで EOS トークンが即座に出力されることがあります。特に「N個の水素結合ドナーを持つ分子を生成」のような抽象度の高いプロンプトで発生しやすい傾向がありました。対策として:
-
プロンプトの具体化:
Generate a molecule with 4 HBD.よりもWhat is the SMILES of ibuprofen?のような具体的な指示の方が安定します - リトライ機能:自作 API Server ではシード変更 + 温度昇温による自動リトライを実装済み
- より高精度の量子化:Q5_K_M や Q8_0 を使用することで改善が期待できます(要メモリ)
おわりに
NatureLM は科学ドメインに特化したユニークなモデルですが、ローカル環境での利用にはカスタムトークナイザーの扱いに注意が必要です。
- Lemonade Server:AMD GPU の Vulkan バックエンドで CPU 比約3倍の高速推論が可能。OpenAI 互換 API により既存アプリとの連携も容易。ただし科学トークンのデコード問題あり。
- 自作 API Server:科学トークンの正しいデコード + OpenAI 互換 API + 自動リトライを統合した最も実用的なソリューション。
- llama-cpp-python + HF トークナイザー:ハイブリッド方式で科学トークンを正しくデコードでき、スクリプト利用に適する。
- Ollama:最も手軽だが、科学トークンの制限は Lemonade と同様。
NVIDIA GPU がない AMD 環境でも、用途に応じて方式を使い分けることで NatureLM を実用的に活用できることを確認しました。