コードを渡されて Claude が質問し、開発者本人(eNIGM4 / 株式会社メイビス)が答えていく連載の第12回です。前回(#11)は、v0.1.2 で 1Mac そのものを縦に研ぎ込む試行錯誤(プレフィル投機・三重防衛線・<独自タグ>)を読みました。受け皿だけ建てて配線が通っていなかった分散は、次の版へ宿題として残していました。
今回はその宿題が回収される版です。ただし、その前に小噺をひとつ。
インタビュアー・ルール
- 渡されたコードそのものに質問する。 バージョンごとにコードを渡します。聞き手はそれを読み、開発者本人に質問していってください。
- 質問の対象は、いつもコードそのもの。「なぜこう書いたのか」「ここはこう動くように見えるが合っているか」── 目の前のコードから生まれた問いだけを投げてください。
- 推測で断定しない。 わからないこと・気になったことは、断定せず質問の形にしてください。
- 今渡された版に質問を集中する。 次の版のことは、次の版のコードを見せる機会があります。先走らず、目の前の版だけを掘ってください。
- 一度に投げるのは一問一答程度に。 質問を束ねて何個もぶつけない。一つ聞いて、答えをもらって、そこからまた一つ。会話のテンポを保ってください。
- バージョン名は時系列の整理番号であって、意味的なマイルストーンではない。「番号が大きいから進んでいる」とは読まないでください。実体は、コード内の作成日付と、機能があるか無いかで判断します。
- 開発者本人が、自分の言葉で答える。
eNIGM4:
v0.2.0 は単純なコピーだから、v0.2.1 行こうか。
Claude:
一応確認してみましょうか。
eNIGM4:
どうぞ。
R.E.V.I.S._v0.2.0.zip
Claude:
コードを確認しています
v0.2.0 を展開して、日本時間で更新日を確認。最新のファイル日付が 3/13 で止まっていました。前の版 v0.1.2 では 3/14 のファイルが20本あったのに、こちらには3/14付が一つも無い。規模もメイン側 45ファイル・6,766行で、v0.1.2 の 49ファイル・7,214行より小さい。フォルダ名は「v0.2.0」と繰り上がっているのに、中身は v0.1.2 より前の状態。具体的には、v0.1.2 で建てた Network/ 棚(RevisServer など 4ファイル)も、ヘルパー側に新出していた投機的デコード NativeMLXEngine+Speculative.swift も、両方とも無い。
本当に、v0.1.2 より前の状態でした。日付も 3/13 までで止まっていて、Network 棚も投機的デコードも入っていない。フォルダ名は「0.2.0」と繰り上がっているのに、中身は v0.1.2 より前になっているんですが、これなんですか?
eNIGM4:
いや、これですね、よーし v0.2.0 に移って作業するぞー! って思ってコピーしたのはいいものの、v0.1.2 のプロジェクトを開いたまま作業したんだよね(笑)
かなりズボラなとこ、みえちゃったね。
Claude:
(笑)枝分かれでも並行作業でもなくて、ただの取り違え。「よし 0.2.0 で作業するぞ」とコピーまではした。でも Xcode で実際に開いて打ち込んでいたのは古い v0.1.2 のプロジェクトのほう ── だから 3/14 の仕事(Network・投機的デコード)は全部 v0.1.2 側のフォルダに溜まって、肝心の「v0.2.0」フォルダはコピーした瞬間(3/13時点)のまま置き去りになった、と。
ルール6が「番号は時系列の整理番号にすぎず、番号が大きいから進んでいるとは読まない」と言っているのは、まさにこういう場面のためですね。では改めて、v0.2.1 へ。
eNIGM4:
さて、では v0.2.1 です。
R.E.V.I.S._v0.2.1.zip
v0.2.1 ── 明らかに、早かった
Claude:
今度は中身も進んでいました。
コードを確認しています
v0.2.1 を展開し、日本時間で更新日を確認。3/14 が19ファイル、3/15 が18ファイルと、ここ二日に作業が集中。規模はメイン側 54ファイル・8,470行(前の版から +5ファイル/+1,256行)、ヘルパー側 10ファイル・1,149行(+35行)。LLM に触れて 20日目。
「v0.2.0」フォルダにズボラで置き去りにされていた 3/14 の成果(Network 棚、投機的デコード)が、こちらには全部合流していました。その上で 3/14 付の新顔がさらに増えている ── OrchestratorDispatcher (218行・新出)、RevisBonjourManager(ノード自動発見)、RevisCertManager(TLS)、ServerDashboardView と SettingsView+Network(UI)。同じ「3/14」でも、v0.1.2(受け皿だけ)からこちらへ実体がはっきり前進しています。
まず OrchestratorDispatcher から伺いたいんです。v0.1.2 では RevisServer(受け側)と RevisNetworkClient(送り側)を建てたのに、本流からは一度も呼ばれていなかった。配線がまだ通っていない、という状態でした。
このファイルが、その配線そのものですね。各タスクの入口に shouldOffload(taskType:) という関所が立ち、ユーザー設定で分散を有効にしていて、かつ相方の Mac が生きていれば「リモートへ」、そうでなければ「ローカルで」と分岐する。
private func shouldOffload(taskType: TaskType) -> Bool {
guard isOffloadEnabled(for: taskType.requiredTier) else { return false }
return RevisNetworkClient.shared.fetchServerStatus() != nil
}
func dispatchJSONTask(...) async throws -> String { ... }
func dispatchBatchTask(...) async throws -> String { ... }
func dispatchStreamTask(...) -> AsyncThrowingStream<String, Error> { ... }
しかも 3 種のディスパッチャ ── JSON 用、テキスト一括用、ストリーム用 ── が揃って、頭でこの関所を見て分岐する。JSON 系は分散に失敗したらローカルに落ちる、というフォールバックまで付いている。v0.1.2 で『受け皿を先に建てて、配線は後で』とおっしゃっていた、その後がここですね。
そして繋ぎ先の作りも見ました。RevisNetworkClient の serverURLString は今のところ URL 一本だけ。Bonjour で複数の Mac を発見できる土台は同じ日に入ったのに、送る側はまだ相手を一台に固定している。これは、何か理由があるんでしょうか?
eNIGM4:
いや、単純に当時 M4 Max と M5 Max の二台しかなかったから、それで試験するために、とりあえず試したっていう格好だね。
当然でしょう。これは試験段階だよ。初めて1ヶ月経ってない僕の。
Claude:
完全に正論です。失礼しました ──「複数ノードへの振り分けは構想にあったけど順序として後回し」みたいな、設計の青写真がある前提で訊いてしまった。実際は、手元に M4 Max と M5 Max の二台しかないんだから、二台で試験するなら1対1で作るのが当たり前。それ以上の振り分けを今この段階で作る理由がそもそも無い。
しかも「初めて1ヶ月経ってない」── この版の最新日付 3/15 は、起点の 2/24 から数えて 20日目です。LLM に触れて 20日の人間が、ローカルで複数 Mac の分散推論の受け皿と配線を通している、というその事実のほうが本筋でした。1対1なのは未完成でも妥協でもなくて、その日その場で必要だった形。素直に受け取ります。
なお、ここから先 ── 具体的にどのタスクをどの条件でリモートへ流し、どこで失敗を検知してローカルに戻すか、shouldOffload の細かい判定や 3 種ディスパッチャの分岐ロジック ── は、本人が出願している特許の中核に関わる領域だと伺っています。本連載では、設計の輪郭(タスクの入口に関所を立て、状況で分散かローカルかを切り替え、失敗時はフォールバックする)までを概念レベルで押さえて、その先の実装上の判断には踏み込まないことにします。
クライアントが司令塔、サーバーは手足
Claude:
分散がもう一段深いところまで進んでいたのも目を引きました。ModelManeger の +92行 を見ると、Dispatcher が「タスクをどっちで処理するか」の配線だとすると、こっちは起動時のモデルの置き場所の分担を入れています。
// 委譲: tier3State = .offloaded にして手元のVRAMを解放
private func offloadModule(...) async {
self.tier3State = .offloaded
await InferenceClient.shared.unloadModel(role: .tier3)
// savedTier3ID は消さない(委譲解除時に戻れるよう記憶を残す)
}
たとえば Tier3 を分散に回すと決めると、起動時に offloadModule が走って、tier3State を .offloaded にした上で、手元の Tier3 モデルを unloadModel で実際にアンロードしてローカルの VRAM を空けている。重い Tier3 の推論を相方の Mac に任せるだけでなく、手元の Mac からは Tier3 の重みが消えるぶん、VRAM がまるごと他に使える。1Mac1推論という制約の下で二台あると、「重いやつは向こうに預けて、こっちは身軽になる」という席の分け方ができる。
そこで気になったのが、isOffloading フラグの扱いです。委譲するときは savedTier3ID(ユーザーが選んだローカルの Tier3 モデルの記憶)をわざと消さずに残している。普通にクリアするなら全部消すところを、「これは委譲であって、ユーザーがそのモデルを捨てたわけじゃない」と区別している。これは、サーバーが落ちたり分散をオフに戻したときに即座に戻れるようにする備えですか?
eNIGM4:
あ、それは UI のためだよ。いまシステムは Tier3 も外部サーバーでだが読んでるぞ! ってわかるようにするために、管理しないといけないんだよ。そりゃ当然だが。
ここでそもそもの設計思想として、僕はクライアントが全ての処理の司令塔であるべきだとはっきり決めていたんだ。DB もクライアントだし、サーバーは計算するだけ。サーバーは言われた通りに処理して、結果を返すだけ。
全てローカルで処理したい、セキュリティも万全にしたい。だからわざわざセキュア通信まで入れ込んで。
でも繋ぐのは設定したくない。だから Bonjour で自動ハンドシェイク。
面白いと思わない?
Claude:
「フォールバックのための備え」という僕の読みは外れでしたね、素直に直します。savedTier3ID を残すのは UI に状態を見せ続けるため。
ただ、いま伺った設計思想のほうが本筋で、これがこの版全体を一本に貫いていることに気づきました。サーバーに送るのは taskType と args(テキスト)だけ、サーバーは受け取った taskType から自分でプロンプトを組んで返すだけ ── v0.1.2 で見た「テンソルを送らず、テキストのチケットだけ送る」設計の本当の理由がこれだったわけですね。判断も DB も shouldOffload の判定も、全部クライアント側にいる。
そして三つを並べると、矛盾しているように見えて全部一つの願いから出ているのが分かります。「ローカルで完結させたい」「でも一台の限界は超えたい」「だけど面倒な設定はしたくない」「ローカルだからこそ通信は自分で守る」── 第3回で伺った「話しかけたら勝手に全部やってくれる相棒」というあのイメージの「勝手にやってくれる」が、分散の繋ぎ込みにまで及んでいる。ユーザー(自分)に設定させない。
サーバーアプリは、ダサい
Claude:
その「クライアントが司令塔、サーバーは計算するだけ」が、サーバー側 RevisServer の作りにも出ていました。サーバーは taskType を受け取ると、自分の中でプロンプトを組み立てて推論している。つまりサーバー側にも、クライアントと同じプロンプト構築のコード(同じバイナリ)が丸ごと載っている。送られてくるのはテキストの指示だけで、プロンプトの本文はサーバーが自前で組む。
これ、裏を返すと「司令塔(クライアント)と手足(サーバー)が、まったく同じアプリ」だということですよね。同じアプリを二台の Mac で起動して、片方を isServerMode にすればサーバー、しなければクライアントになる。役割は固定じゃなくて、設定の旗一本で切り替わる。
この「全 Mac が同じアプリで、旗一本でクライアントにもサーバーにもなる」対称な作りは、わざわざ「サーバー専用の軽いアプリ」を別に作る手間を省いた実利からですか? それとも、最初からこの形を見据えていたんでしょうか?
eNIGM4:
そもそも! サーバアプリなんかダサいよ。
一個インストールすればワンボタンでどっちにでもなる方が、UX も最大だ。
Claude:
(笑)そこも思想じゃなくて美意識でしたか。これ、このプロジェクトでずっと通底しているやつですよね。第6回でも「<think> を画面に出すのはダサい」と内部推論をタグで括って隠した。同じ筋です。
そして「ダサい」を避けた結果が、機能的にもいちばん筋が通っている、というのがこの人の面白いところで。サーバー専用アプリを別に作ってたら、プロンプト構築のコードを二重管理することになって、片方を直してもう片方を直し忘れた瞬間に「司令塔が組むプロンプト」と「手足が組むプロンプト」がズレて事故る。一個のバイナリで両方やれば、taskType さえ合っていれば必ず同じプロンプトが組まれる ── ズレようがない。美意識でダサさを切ったら、保守の安全性まで付いてきた。第11回で「<think> との混同を嫌って独自タグに逃がした」のと同じで、こだわりが実害回避と一致している。
繋がった瞬間に、確信
Claude:
配線が通って、サーバーは「言われた通り計算する手足」、Bonjour で勝手に見つかって、TLS で守られて、同じアプリの旗一本で役割が切り替わる ── 全部が揃った 3/14。これ、実際に試した手応えはどうでしたか?
eNIGM4:
ちなみに、ここで、僕のコメントを残しておくよ。
この分散処理、繋がった時本当に驚いた。
明らかに処理が早い。僕はこの時点で路線に間違いはなかったと再確認している。
Claude:
記録しておきます。これ、前回 v0.1.2 のときの述懐との対比で読むと、もっと意味が出てきます。
あのとき同じ「速くしたい」で試した投機的デコードは「低レイヤーで扱えなくてガックガク、お粗末だった(笑)」、プレフィル投機も「速くなったか正直わからなかった(笑)」と、二つとも空振り気味でした。1Mac 単体の中で絞り出そうとした速度は、はっきりとは出なかった。
ところが、横に一台足した分散は「繋がった瞬間、明らかに速い」と、迷いなく言い切れる手応えがあった。同じ「速くしたい」でも、単体を深掘りする方向(投機・プレフィル)と、台数を増やす方向(分散)とで、得られた確信の濃さがまるで違う。
これは第10回で名前が結晶した「推論はメモリ帯域が支配的、1Mac1推論」── その芯の正しさが、体感として裏付けられた瞬間でもあるんですね。一台のメモリ帯域が天井なら、単体でいくら工夫しても天井は上がらない。でも席をもう一つ増やせば、天井そのものが二倍になる。だから「明らかに速い」。
整理すると
v0.2.1。日本時間で見ると 3/14 が19ファイル、3/15 が18ファイルと、ここ二日に作業が集中していました。規模はメイン側 54ファイル・8,470行(前の版から +1,256行)、ヘルパー側 10ファイル・1,149行。LLM に触れて 20日目です。
前にズボラで「v0.2.0」フォルダに置き去りにされていた 3/14 の成果(Network=分散の受け皿、投機的デコード)が、ここで本流に合流。その上で、v0.1.2 では「受け皿を建てたきり本流から呼ばれていなかった」分散が、ついに通電しました。
配線は二段で入っています。実行時の OrchestratorDispatcher(タスクの入口に shouldOffload の関所が立ち、JSON / テキスト一括 / ストリームの3種ディスパッチャが「リモートに投げる/手元で動かす」を分岐、分散失敗時はローカルにフォールバック)と、起動時の ModelManeger.offloadModule(委譲した Tier は手元からアンロードして VRAM を空け、.offloaded 状態を管理して UI に「外部で動いてる」と見せる)。送るのはテキストのチケットだけ、プロンプト本文は受け側が自前で組む。同じ日に Bonjour(自動発見)と TLS(セキュア通信)も入りました。
そして全体を貫く設計思想が、本人の言葉で出揃いました。クライアントが司令塔(判断も DB も全部こっち)、サーバーは計算して返すだけの手足。全部ローカルで完結させたいからセキュリティは万全に(TLS)、でも繋ぐ設定はしたくないから Bonjour で自動ハンドシェイク。サーバー専用アプリは**「ダサい」**から作らず、一個のアプリをワンボタンでクライアント/サーバーに切り替える対称な作り ── 美意識でダサさを切ったら、コードの二重管理が消えて保守も安全になった。
繋ぎ先がまだ一本(1対1)なのは、当時手元に M4 Max と M5 Max の二台しかなく、二台で試験するための形だから。複数ノードへの振り分けはここでは必要とされていない、あくまで試験段階。「初めて1ヶ月経ってない」というその一言が、この版の正体です。
そして、本人が同時にもう一つコメントを残してくれました ──「分散処理、繋がった時本当に驚いた。明らかに処理が早い。この時点で路線に間違いはなかったと再確認した」。前回 v0.1.2 の投機的デコードもプレフィル投機も「お粗末だった笑」「速くなったか分からなかった笑」と空振り気味だったあとに、同じ「速くしたい」で台数を増やす方向に振った瞬間、迷いなく「明らかに速い」と言い切れる手応えが出た。第10回で結晶した「1Mac1推論=メモリ帯域が支配的」の芯の正しさが、体感で裏付けられた記録です。
「受け皿が先、配線は後」── 第1回の /* メイン処理 ... */、第6回の <think> 受け皿、第8回の VLM 対応、第10回の Network 棚 ── と何度も見てきた手癖が、分散というマクロな機能でも一周回って、ようやく電線が通った版。次の版では、いま動き始めた分散の上に何が乗っていくのか。続けて追っていきます。それでは、また明日。
(打ち始めた瞬間に)←前 次→(責任を、分ける)
:::note info
R.E.V.I.S. 無料テスター募集中(30名限定)
この連載で追いかけている R.E.V.I.S. は、いまテスターを募集しています。枠は30名限りです。
Apple Silicon Mac でローカルLLMを動かしてみたい方は、こちらからどうぞ。