前シリーズ(構築編/不安定化の切り分け編/3モデル比較編)の続きです。
1台の DGX Spark(GB10, 128GB ユニファイドメモリ)で TensorFold + LiteLLM + opencode を運用。
前編「[Qwen3.8-Flash-Next と格闘した記録]」で Flash-Next のループに敗れ、常用を Qwen3.8-27B に妥協。
そこから 運用基盤・サンプリング・高速化を詰めて、結果的に「正解」に落ち着いた記録です。
数値は手元1台の実測。IP・ホスト名・認証情報はプレースホルダに置き換えています。
TL;DR
- Flash-Next を諦め Qwen3.8-27B(MLX 4-bit + DFlash2) を常用に。安定・速度・素直さで正解だった。
-
サンプリングは公式準拠(thinking/non-thinkingで値が別。非thinkingは
presence_penalty=1.5)。 - 高速化で効いたのは
--prefill-fp8(prefill +33%) と--checkpoint-slots+--spill-gib(prefix保持)。 -
効かなかったのは
--mtp-drafts/--drafter-bits(受入数が完全同一=投機の追加チューニングは無効)。 - イメージは Python系 v0.6.6 が正解(ネイティブZig v1.0.x は27B未移植+Pythonより遅い)。
- 運用の落とし穴は
docker composeの profile とlogs(profile配下はlogsに出ない)。
1. なぜ 27B に落ち着いたか
前編の結論(Flash-Next は fast でループ)を受け、常用を 27B に切替。結果:
| モデル | 能力 | 速度 | 安定/使い勝手 |
|---|---|---|---|
| Qwen3.8-27B(採用) | 中〜高 | 速い | ◎(枯れている) |
| Qwen3.8-Flash-Next | 高 | 長文脈で健闘 | △(fastループ、think前提) |
- 27B は dense 27B。MLX 4-bit + DFlash2 で単機でも実用的な速度。
- 短文脈で decode ~148 tok/s、長文脈(150k超)で ~50 tok/s(denseは文脈長でKV読みが増える)。
- 素直に指示に従う(ループしない)。エージェント用途では「賢さ」より「素直さ」が効く。
「妥協」と書きましたが、常用の安定性では27Bが最良のデフォルトでした。
2. 運用基盤: profile切替と docker compose logs の罠
1GPUなので「同時に1モデル」。compose の profile で切替ます。
services:
tensorfold: # 既定 = Qwen3.8-27B (container: tensorfold)
tensorfold-flashnext: # profiles: ["flashnext"]
tensorfold2: # profiles: ["tf36"] = Qwen3.6-35B-A3B
cd /opt/docker/TensorFold
docker compose up -d # 27B(既定)
docker compose stop tensorfold && docker compose --profile flashnext up -d tensorfold-flashnext
docker compose stop tensorfold && docker compose --profile tf36 up -d tensorfold2
落とし穴: docker compose logs -f はprofile未指定だと profile配下のサービスを出さない。
- 稼働中モデルが profile配下だと、
logsが空に見える(「起動しているのにログが出ない」)。 - 対策: 既定サービスを常用モデルに固定し、コンテナ名を固定(
tensorfold)。
あるいはdocker compose --profile <name> logs -f <svc>。
3. opencode 側のチューニング
opencode.jsonc を調整しました。
| 項目 | 変更 | 意図 |
|---|---|---|
compaction.buffer |
8000 → 48000 | 出力トークン(32768)を含めても上限超過しないよう余白確保 |
limit.context |
サーバ窓に整合(例 240000) | 宣言が実容量を超えると「too long → 圧縮」多発 |
small_model |
停止中のホスト → 稼働中 | title生成などの補助タスク失敗を解消 |
headerTimeout/chunkTimeout
|
既定(5分) → 600000/900000 ms | 長い prefill でも切れにくく |
- 症状の多くは 「コンテキスト上限超過の BadRequest」 と 「停止ホスト宛の接続失敗」 でした。
4. サンプリングは「公式準拠」が最重要(前編の反省を27Bにも)
Qwen3.8(27B / Flash-Next 共通)のモデルカードの推奨値に統一します。
-
Thinking:
temperature=1.0, top_p=0.95, top_k=20, min_p=0.0, presence_penalty=0.0, repetition_penalty=1.0 -
Instruct(非thinking):
temperature=0.7, top_p=0.80, top_k=20, min_p=0.0, **presence_penalty=1.5**, repetition_penalty=1.0
litellm と opencode の両方を公式値に統一。
# litellm-config.yaml(非thinkingルートの例)
extra_body:
chat_template_kwargs:
enable_thinking: false
temperature: 0.7
top_p: 0.8
top_k: 20
min_p: 0.0
presence_penalty: 1.5
repetition_penalty: 1.0
教訓: 推論パラメータは体感でなく公式カードで突き合わせる。特に non-thinking の
presence_penaltyは見落としやすい。
5. 高速化の実測(27B)
TensorFold の27Bで各フラグを測定しました。
| 施策 | 結果 |
|---|---|
--prefill-fp8 |
prefill +33%(1,364 → 1,815 tok/s @30k cold)。decodeは不変 |
--checkpoint-slots 16+--spill-gib 32 |
prefix保持が向上(6会話を挟んだ後でも再送は 99.9% cached・ttft 0.12s) |
--mtp-drafts 6 |
効果なし(受入数が完全同一) |
--drafter-bits 0 |
効果なし(同上) |
- 効いた: prefill-fp8(プロンプト処理)と prefix保持(再prefill削減)。
- 効かなかった: 投機デコード(DFlash2)の追加チューニング。受入数が完全に同一で、フラグが投機経路を変えていませんでした。
- 最終構成:
tensorfold serve TensorFold/Qwen3.8-27B-MLX-4bit --name Qwen3.8-27B --host 0.0.0.0
--parallel 8 --drafter z-lab/Qwen3.8-27B-DFlash2
--prefill-fp8 --checkpoint-slots 16 --spill-gib 32
長文脈の decode(~50 tok/s)を直接上げるのは文脈圧縮が本命、という整理も得ました(未着手)。
6. イメージ選定: v0.6.6 が正解
-
現行 = TensorFold v0.6.6(Python系) = Pythonラインの最新リリース。
-
最新 = v1.0.7(ネイティブZig
tensorfold-native) だが、CLI/対応モデルが別。 -
公式リリースノート(v1.0.0)に決定的な記述:
Qwen3.8-27B ... being ported for 1.0.x. Qwen3.8-27B passes drafted/plain equality, but remains outside 1.0.0 because its paired served decode and cold-prefill results are slower than Python 0.6.6.
-
→ 27BをGB10で使うなら v0.6.6 が正解。v1.0.xへ上げても速くならず、むしろ遅い/未対応。
7. モデル/チェックポイント選定
モデル
| モデル | 能力 | 速度 | 安定 | 位置づけ |
|---|---|---|---|---|
| Qwen3.8-27B | 中〜高 | 速い | ◎ | 安定・速度の万能デフォルト |
| Qwen3.8-Flash-Next | 高 | 長文脈で健闘 | △(fastループ、think前提) | 品質重視 |
| Qwen3.6-35B-A3B | 中 | 最速 | ○ | 軽量枠 |
| GLM 5.3 Flash | 高 | 速い | 要導入 | 対抗馬 |
27B のチェックポイント
| チェックポイント | 速度(GB10) | 品質 | 備考 |
|---|---|---|---|
| MLX 4-bit(採用) | ◎最速 | △〜○(難問で低下の報告) | DFlash2対応 |
NVFP4(nvidia/RadixArk) |
○(遅め) | ○〜◎ | 統制研究でMLXより正答率高(18/24 vs 14/24) |
| FP8 | △(重い, ~30GB) | ◎ | 単機では不利 |
- 速度重視(opencodeエージェント)なら MLX 4-bit。品質重視なら NVFP4 に切替えてA/B。
8. まとめ
- 常用は 27B が最良のデフォルト(Flash-Nextはthink前提で常用しにくい)。
-
サンプリングは公式準拠(特に non-thinking の
presence_penalty=1.5)。 - 高速化は prefill-fp8 と prefix保持が効く(投機の追加チューニングは効かない)。
- イメージは v0.6.6(Python)が正解。
-
profile運用では
docker compose logsに注意(既定サービスを常用モデルに固定)。
「妥協」から始めた 27B ですが、素直さ・安定性・速度のバランスで常用の正解でした。
前編と合わせて、単機DGX Spark でローカルLLMを常用するまでの実践記録になっています。
数値は筆者環境での実測です。ドライバ・イメージ・チェックポイント・負荷で変動します。