DGX Spark(GB10, 128GB ユニファイドメモリ)上に、推論エンジン TensorFold、APIゲートウェイ LiteLLM、
UI Open WebUI、そしてコーディングエージェント opencode から使えるローカルLLM環境を構築する手順です。
LiteLLM を挟んでいる理由は「opencode から Thinking をオフにできない」問題にあります(後述)。
本記事は「構築編」。続編として「不安定化の切り分け編」「3モデル比較編」があります。※ IPアドレス・ホスト名・認証情報はプレースホルダに置き換えています。
全体構成
[opencode クライアント] (別ホスト)
│ OpenAI 互換 API (http)
▼
[LiteLLM :4000] ── model_list でルーティング ──┐
│ │
▼ ▼
[TensorFold :8080] (Qwen3.8-27B) [TensorFold2 :8080] (Qwen3.6-35B-A3B)
│ │
└──────────── NVIDIA GB10 (128GB UMA) ───┘
- TensorFold: 投機デコード(speculative decoding)を備えた OpenAI 互換の推論サーバ。Docker で動作。
-
LiteLLM: 複数モデルを
fast/thinkなどの名前で束ね、タイムアウト・並列度・リトライを一括管理。 - Open WebUI: ブラウザUI。
- opencode: LiteLLM の OpenAI 互換エンドポイントをそのまま利用。
この構成にした理由(LiteLLM を挟む動機)
本構成の出発点は 「opencode から Thinking(思考)をオフにできない」問題 です。
- Qwen3.8 系は既定で thinking オン。TensorFold ではリクエストボディで
chat_template_kwargs: { "enable_thinking": false }を送ることでオフにできます。 - ところが利用時点の opencode の設定形式では、この
chat_template_kwargsを
リクエストボディへ自由に注入できず、常に thinking オンで動作 → 応答が遅い。- 実際には opencode の V1系も併用しており、V1 の設定形式(
options)では
任意のリクエストボディ項目を渡せないため、thinking をオフにできませんでした。 - (V2系でも、
bodyを使わない構成では同様に注入できないケースがあります。)
- 実際には opencode の V1系も併用しており、V1 の設定形式(
- そこで LiteLLM をゲートウェイとして挟み、モデルルート(
qwen3.8-fast)単位で
extra_body.chat_template_kwargs.enable_thinking: falseを強制注入する構成にしました。
# litellm-config.yaml(該当部分)
- model_name: qwen3.8-fast
litellm_params:
model: openai/Qwen3.8-27B-exl3
api_base: http://tensorfold:8080/v1
api_key: dummy
extra_body:
chat_template_kwargs:
enable_thinking: false # ← opencode からは送れないのでここで強制
- 副次的な利点として、タイムアウト・並列度・リトライも LiteLLM 側で一元管理できます。
- opencode 側は
qwen3.8-fastを選ぶだけで、thinking オフの高速モデルとして利用できます。
前提
| 項目 | 内容 |
|---|---|
| ハード | NVIDIA DGX Spark(GB10, Grace-Blackwell, 128GB ユニファイドメモリ) |
| OS | DGX OS(Ubuntu ベース, aarch64) |
| ランタイム | Docker + NVIDIA Container Toolkit |
| 推論エンジン | TensorFold(NVIDIA PyTorch コンテナベースのイメージ) |
| クライアント | opencode(V1系・V2系を併用) |
GB10 は CPU/GPU がメモリを共有(ユニファイドメモリ) します。ここが後続記事のキモになります。
1. ディレクトリ構成
/opt/docker/TensorFold/
├── docker-compose.yml
├── litellm-config.yaml
├── Dockerfile
├── models/ # ローカルに置いたチェックポイント
│ └── qwen38-27b-exl3-4.00bpw/
└── cache/ # Hugging Face キャッシュ(重み・ドラフタ)
./models と ./cache をコンテナへマウントして使います。
Dockerfile
compose の build: . から使う Dockerfile です。NVIDIA PyTorch コンテナをベースに、
TensorFold 本体を pip で入れるだけのシンプルな構成です。
FROM nvcr.io/nvidia/pytorch:26.07-py3
ENV PIP_NO_CACHE_DIR=1 \
TENSORFOLD_NO_UPDATE_CHECK=1
# HF_HOME は設定しない(compose の /root/.cache マウントと既定 ~/.cache/huggingface を使う)
RUN python -m pip install --no-cache-dir \
"git+https://github.com/ashhart/TensorFold.git@v0.6.6"
RUN tensorfold --version && \
python -c "import torch; print('torch', torch.__version__, 'cuda', torch.version.cuda)"
WORKDIR /data
EXPOSE 8080
# CMD にしておく → compose の command で丸ごと上書きできる
CMD ["tensorfold", "serve", "--help"]
ポイント:
- ベースは NVIDIA PyTorch コンテナ(CUDA・ドライバ互換が揃っている)。
-
TENSORFOLD_NO_UPDATE_CHECK=1で更新チェックを抑止(外部網に出られない環境向け)。 -
CMDは--helpにしてあり、compose のcommandで丸ごと上書きする前提。
2. docker-compose.yml
2つの推論サーバ(27B と 35B-A3B)と LiteLLM を1ファイルで定義します。
services:
tensorfold:
build: .
image: tensorfold-dgx:v0.6.6
container_name: tensorfold
devices:
- nvidia.com/gpu=all
volumes:
- ./cache:/root/.cache
- ./models:/models
restart: unless-stopped
shm_size: "16gb"
command:
- tensorfold
- serve
- /models/qwen38-27b-exl3-4.00bpw # 27B dense
- --name
- Qwen3.8-27B
- --host
- 0.0.0.0
- --parallel
- "8" # 同時デコード(CUDA既定は直列)
- --drafter
- z-lab/Qwen3.8-27B-DFlash2 # 投機デコード用ドラフタ
environment:
- TENSORFOLD_MEMORY_RESERVE_GIB=4
- TENSORFOLD_CUDA_MEMORY_LIMIT_GB=64 # このコンテナのCUDAメモリ上限
tensorfold2:
build: .
image: tensorfold-dgx:v0.6.6
container_name: tensorfold2
devices:
- nvidia.com/gpu=all
ports:
- "8081:8080"
volumes:
- ./cache:/root/.cache
- ./models:/models
restart: unless-stopped
shm_size: "8gb"
command:
- tensorfold
- serve
- TensorFold/Qwen3.6-35B-A3B-MLX-4bit-MTP
- --name
- Qwen3.6-35B-A3B
- --mtp-drafts
- "8" # MTP(自己投機)のドラフト数
- --host
- 0.0.0.0
- --context
- "131072"
environment:
- TENSORFOLD_MEMORY_RESERVE_GIB=4
- TENSORFOLD_CUDA_MEMORY_LIMIT_GB=44
litellm:
image: ghcr.io/berriai/litellm:main-latest
ports:
- "4000:4000"
environment:
LITELLM_MASTER_KEY: "sk-local-xxxxx"
volumes:
- ./litellm-config.yaml:/app/config.yaml
command: ["--config", "/app/config.yaml"]
restart: unless-stopped
depends_on:
- tensorfold
- tensorfold2
各フラグの意味
| フラグ | 説明 |
|---|---|
--parallel N |
同時にデコードするリクエスト数。**CUDA では既定が「1件ずつ直列」**なので、エージェント用途では明示推奨 |
--drafter <repo> |
外部ドラフタ(DFlash2 など)。投機デコードで decode が数倍に |
--mtp-drafts N |
モデル内蔵の MTP(Multi-Token Prediction)ヘッドで投機 |
--context N |
prompt + reply の窓。絞るとKVキャッシュ(=メモリ)を節約できる |
TENSORFOLD_CUDA_MEMORY_LIMIT_GB |
コンテナのCUDAメモリ上限(起動見積りより大きくする) |
TENSORFOLD_MEMORY_RESERVE_GIB |
起動時に確保せず残すメモリ |
shm_size |
共有メモリ。推論では控えめでよい |
3. litellm-config.yaml
fast(thinking を強制オフ)と think(thinking オン)を分けて定義します。
fast 側の extra_body.chat_template_kwargs.enable_thinking: false が、本構成の肝です
(opencode からは送れないため LiteLLM で注入)。
model_list:
- model_name: qwen3.8-fast
litellm_params:
model: openai/Qwen3.8-27B-exl3
api_base: http://tensorfold:8080/v1
api_key: dummy
max_parallel_requests: 8
extra_body:
chat_template_kwargs:
enable_thinking: false # 思考オフ=速い
- model_name: qwen3.8-think
litellm_params:
model: openai/Qwen3.8-27B-exl3
api_base: http://tensorfold:8080/v1
api_key: dummy
max_parallel_requests: 8
- model_name: qwen3.6-fast
litellm_params:
model: openai/Qwen3.6-35B-A3B
api_base: http://tensorfold2:8080/v1
api_key: dummy
max_parallel_requests: 8
extra_body:
chat_template_kwargs:
enable_thinking: false
- model_name: qwen3.6-think
litellm_params:
model: openai/Qwen3.6-35B-A3B
api_base: http://tensorfold2:8080/v1
api_key: dummy
max_parallel_requests: 8
litellm_settings:
request_timeout: 1800 # 長いprefill/生成を切らない
stream_timeout: 1800
num_retries: 2
max_parallel_requestsはサーバ側--parallelと揃えるのがコツ。
サーバだけ上げてもゲートウェイが低いと 429 になります。
4. opencode 側の設定
4.1 V1形式(実際に使っていた設定)
~/.config/opencode/opencode.jsonc。V1系の形式で、provider.<id>.npm と options、models、enabled_providers、small_model などを使います。
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"DGXtensorfold": {
"npm": "@ai-sdk/openai-compatible",
"name": "Local TensorFold",
"options": {
"baseURL": "http://<DGX_SPARK>:4000/v1",
"apiKey": "sk-local-xxxxx"
},
"models": {
"qwen3.8-fast": {
"name": "qwen3.8-fast",
"tool_call": true,
"tools": true,
"reasoning": false,
"limit": { "context": 262144, "output": 32768 },
"options": { "temperature": 0.6, "presence_penalty": 0.0, "reasoning_effort": "none" }
},
"qwen3.8-think": {
"name": "qwen3.8-think",
"tool_call": true,
"tools": true,
"limit": { "context": 262144, "output": 32768 }
},
"qwen3.6-fast": {
"name": "qwen3.6-fast",
"tool_call": true,
"tools": true,
"limit": { "context": 131072, "output": 32768 }
},
"qwen3.6-think": {
"name": "qwen3.6-think",
"tool_call": true,
"tools": true,
"limit": { "context": 131072, "output": 32768 }
}
}
}
},
"enabled_providers": ["DGXtensorfold"],
"small_model": "qwen3.6-fast",
"compaction": { "buffer": 48000 },
"agent": { "explore": { "disable": true }, "general": { "disable": true } },
"autoupdate": false,
"share": "disabled",
"watcher": { "ignore": ["node_modules/**", "dist/**", ".git/**", "*.log"] },
"permission": {
"edit": "allow",
"bash": { "*": "allow", "rm -rf *": "deny", "sudo *": "ask" },
"webfetch": "deny",
"skill": { "*": "allow" }
},
"instructions": ["./AGENTS.md"]
}
V1形式の限界(本構成の動機): この形式では options に chat_template_kwargs のような
任意のリクエストボディ項目を渡せず、TensorFold/Qwen の thinking をオフにできません。
そこで LiteLLM 側で enable_thinking: false を注入しています(前述)。
4.2 V2形式(参考)
V2 ではプロバイダを providers(複数形)で書き、モデルは settings / body / limit を使います。
body を使えば任意のリクエストボディ項目をマージできるため、LiteLLM を挟まずに thinking をオフにできる可能性があります。
{
"$schema": "https://opencode.ai/config.json",
"providers": {
"DGXtensorfold": {
"package": "@opencode/ai/providers/openai-compatible",
"settings": {
"baseURL": "http://<DGX_SPARK>:4000/v1",
"apiKey": "sk-local-xxxxx",
"headerTimeout": 600000,
"chunkTimeout": 900000
},
"models": {
"qwen3.8-fast": {
"body": { "chat_template_kwargs": { "enable_thinking": false } },
"limit": { "context": 262144, "output": 32768 }
}
}
}
},
"compaction": { "buffer": 48000 }
}
-
headerTimeout/chunkTimeout: 応答ヘッダ/ストリーム間の待ち時間(ms)。既定5分だと長い prefill で切れやすい。 -
compaction.buffer: 自動コンパクションの余白。出力トークン数より大きくしないと上限超過で 400 になりがち。
5. メモリ配分の考え方(超重要)
GB10 はユニファイドメモリなので、各コンテナの上限の合計が実メモリを超えると危険です。
MemTotal ≈ 121 GiB
├── tensorfold (27B): CUDA上限 64 GiB(実使用 38〜60 GiB)
├── tensorfold2 (35B): CUDA上限 44 GiB(実使用 20〜32 GiB)
├── shm: 16 + 8 GiB
└── OS・その他
- 上限はあくまで「天井」ですが、ピーク時に両方が伸びると合計が実容量を超え、
NV_ERR_NO_MEMORY→ コンテナ再起動に至ります。 - 併存させるなら 合計に余白(20GiB 以上) を残すか、片方を止める前提で設計しましょう(→ 続編「切り分け編」)。
6. 起動と疎通確認
cd /opt/docker/TensorFold
docker compose up -d
docker compose ps
# LiteLLM のモデル一覧
curl -s http://localhost:4000/v1/models \
-H 'Authorization: Bearer sk-local-xxxxx'
# 疎通(thinking オフ)
curl -s http://localhost:4000/v1/chat/completions \
-H 'Authorization: Bearer sk-local-xxxxx' -H 'Content-Type: application/json' \
-d '{"model":"qwen3.8-fast",
"messages":[{"role":"user","content":"Reply with exactly: OK"}],
"max_tokens":16}'
起動ログで見積りを確認しておくと安全です。
docker logs tensorfold | grep -E 'startup estimate|serving'
# 例: CUDA rank 0 startup estimate 38.39 GiB within 64.00 GiB; ...
# serving Qwen3.8-27B at http://0.0.0.0:8080/v1 on CUDA ... loaded in 6.7s
まとめ
- TensorFold ×2 + LiteLLM で、27B と 35B-A3B を同時にOpenAI 互換で提供できます。
- LiteLLM を挟む主目的は、opencode から送れない
enable_thinking: falseを注入して thinking をオフにすること。 - コツは (1)
--parallelを明示、(2) LiteLLM の並列度を揃える、(3) ユニファイドメモリの合計に余白を残す。 - 次は「不安定化の切り分け編」で、実際にメモリ逼迫で落ちた事例を扱います。
シリーズ
- 前編: 構築編(本記事)
- 中編: 不安定化の切り分け
- 後編: TensorFold で3モデル比較(量子化・形式の違い)