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

【DGX Spark】TensorFold + LiteLLM + opencode でローカルLLM環境を構築する

0
Last updated at Posted at 2026-10-11

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 を使わない構成では同様に注入できないケースがあります。)
  • そこで 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) ユニファイドメモリの合計に余白を残す。
  • 次は「不安定化の切り分け編」で、実際にメモリ逼迫で落ちた事例を扱います。

シリーズ

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