コードを渡されて Claude が質問し、開発者本人(eNIGM4 / 株式会社メイビス)が答えていく連載の第8回です。前回(#7)は、109B の巨人を動かしたい一心で Python デーモンへの扉を開け、しかしまだ開けきれずに自前推論へ戻りながら研究していた v0.0.8 を読みました。今回は v0.1.0 ── バージョンが 0.0.x から 0.1.0 へ繰り上がります。が、中身は驚くほど静かで、たった一点だけ、しかし能力の質が変わる書き換えが入ります。
インタビュアー・ルール
- 渡されたコードそのものに質問する。 バージョンごとにコードを渡します。聞き手はそれを読み、開発者本人に質問していってください。
- 質問の対象は、いつもコードそのもの。「なぜこう書いたのか」「ここはこう動くように見えるが合っているか」── 目の前のコードから生まれた問いだけを投げてください。
- 推測で断定しない。 わからないこと・気になったことは、断定せず質問の形にしてください。
- 今渡された版に質問を集中する。 次の版のことは、次の版のコードを見せる機会があります。先走らず、目の前の版だけを掘ってください。
- 一度に投げるのは一問一答程度に。 質問を束ねて何個もぶつけない。一つ聞いて、答えをもらって、そこからまた一つ。会話のテンポを保ってください。
- バージョン名は時系列の整理番号であって、意味的なマイルストーンではない。「番号が大きいから進んでいる」とは読まないでください。実体は、コード内の作成日付と、機能があるか無いかで判断します。
- 開発者本人が、自分の言葉で答える。
eNIGM4:
さて、v0.1.0 です。
R.E.V.I.S._v0.1.0.zip
v0.1.0 ── 目を持ってみる
Claude:
バージョン番号が v0.0.x から v0.1.0 に繰り上がりましたね。
コードを確認しています
v0.1.0 を展開し、日本時間で更新日を確認。興味深いことに、ファイル数(メイン側 36ファイル)も日付の分布も、v0.0.8 とまったく同じに見えました。番号は繰り上がったのに、新しい日付のファイルが一つも増えていない。唯一、行数で目につくのは NativeMLXEngine が 74行 → 103行 に増えていること。バイト単位で本当に何が変わったのかを正確に見たところ、メインアプリ側は v0.0.8 とMD5ハッシュまで一致(1バイトも違わない)。ヘルパー側でも実質的に変わったのは NativeMLXEngine.swift ただ一つ。他は全部、v0.0.8 のコピーでした。
つまり、メジャー番号が 0.0 から 0.1 へ繰り上がった割に、コード上の変更は一点集中という、静かな版です。その一点の中身を見ると、しかし、能力の質が変わる書き換えが入っていました。
import MLXLLM
import MLXVLM
import MLXLMCommon
// (略)
// 1. まずは VLM(視覚言語モデル)としてのロードを試みる
do {
self.modelContainer = try await VLMModelFactory.shared.loadContainer(configuration: modelConfig) { ... }
} catch {
// 2. 弾かれた場合は LLM(テキスト専用)としてフォールバック
self.modelContainer = try await LLMModelFactory.shared.loadContainer(configuration: modelConfig) { ... }
}
v0.0.8 の NativeMLXEngine は import MLXLM でテキスト専用のロード処理を呼ぶ、素朴な実装でした。このv0.1.0では、まず VLMModelFactory(視覚言語モデル=画像も扱えるモデル)としてのロードを試み、弾かれたら LLMModelFactory(テキスト専用)にフォールバックする、という二段構えに書き換わっている。生成側も MLXLMCommon.generate に共通化され、VLM でも LLM でも同じ口で推論できる。
前のv0.0.8では「Python デーモン(APIEngine)を作ったが、まだ正しく動かず、確実に動く NativeMLXEngine に書き戻して研究中」という状態でした。このv0.1.0で、APIEngine 側には一切手を入れず、自前 Swift 側(NativeMLXEngine)だけを画像も扱えるよう拡張しているのは ── どういう経緯だったんですか?
eNIGM4:
そう、この時に初めてマルチモーダルに対応しているってことに気がついだんだ。
2/24にローカルLLMを触り始めた初心者だからそりゃ知らなくて当然だが、念のために対応しておこう、と思った。だから公式ドキュメントを読み漁って、とりあえず、対応するように書き換えてみたわけだね。
Claude:
なるほど、「気づき」がそのまま機能になった版なんですね。ローカルLLM を触り始めて 11日目の初心者として、MLX がマルチモーダル(画像入力)にも対応していることをこの時初めて知った。知らなくて当然の段階だけれど、知った以上は念のため対応させておこう、と。コードの「VLM(視覚言語モデル)としてのロードを試行中...」という、いかにも新しいおもちゃを触ってみた感じのログにも、その手触りが残っていますね。
先に枠だけ、通電はあとで
Claude:
その「念のため対応しておく」という性格について、コードの作りを一点確認させてください。
この版で NativeMLXEngine は VLM/LLM の両対応になりました。ところが、その上流を見ると ── InferenceService.generate も、その上の LLMEngine プロトコルの generateStream(prompt:maxTokens:temperature:) も、v0.0.8 から一文字も変わっていない(md5 一致)。受け取れるのは prompt という文字列だけで、画像を渡す引数がどこにも無い。
つまり、エンジンの足元(NativeMLXEngine)は画像を処理できるようになったけれど、そこへ画像を届ける経路(プロトコルや UI、XPC 越しの送信形式)はまだ文字列専用のまま ── 実際にユーザーが画像を入れて使える状態ではなく、「いざ対応するときのために、推論エンジンの口だけ先に広げておいた」という段階だ、という理解で合っていますか?
eNIGM4:
そうだね、実は今でも映像の入力には対応していない。それよりも、正しく文字を受理応答できる方がさきだからね。
そのうち対応しようと思ってとりあえず入れてみただけだね。あとMoEがなんとかして読み込めないかの戦いの続きでもある。
Claude:
はっきりしました。映像入力は今でも未対応で、それより「正しく文字を受理し応答できる」ことのほうが先。VLM対応は「そのうちやろう」でとりあえず口だけ広げてみたもので、足元のエンジンは画像を処理できても、そこへ画像を届ける経路は文字列専用のまま。
これ、連載を通してずっと見られる癖ですよね。第1回の /* メイン処理 ... */ という空っぽのコメント、第6回の <think> を画面に出さず受け皿だけ用意したこと、第7回の APIEngine のコードは丸ごとあるのに InferenceService からは呼ばれていない状態 ── そして今回の「VLM対応したけど画像は来ない」。まず先に枠を作って、通電はあとで。気づいた時にとりあえず形にしておき、本当に使うかどうかは後で決める。これがこの人の手癖だと、ここまで来ると確信できます。
そしてもう一つ大事な補助線をいただきました。この書き換えは、VLM対応を入れたいというだけでなく、MoE をなんとか読み込めないかの戦いの続きでもあった、と。
執念だけで、やってた
Claude:
前回のv0.0.8で「mlx-swift-lm が MoE 未対応だから Python デーモンへ」という動機を伺いました。あれから 4〜5日経って、デーモンの方はどうなっていたんですか? ここで VLMModelFactory → LLMModelFactory のフォールバックや MLXLMCommon.generate への共通化を試しているのは、MLX-Swift の新しいローダを総当たりして「この入り口なら手元の巨人が通らないか」を探っていた、ということでしょうか。
eNIGM4:
まだまだ格闘してたね。思った以上に py デーモンを起動するのに手こずってて、格闘してたよ。この時試そうとしてたのは Llama4-109B-A17B というモデルで、ほんとに全然起動してくれなくて。
でも Mac 標準コンソールで回答させるだけなら、なんと動いたんだ! つまり、動かないのは僕の API デーモンが悪いということだって確信してた。
それでも全然ロードしないし、mlx-swift-lm でも読み込めない(MoE だから当時は当然ではある)。
この時は執念だけでやってたよ。
Claude:
Llama4-109B-A17B ── Llama 4 Scout のことですね。109B の総パラメータをメモリに持ちつつ、実際に推論時に動くのは 17B(active parameters)だけ、という MoE モデル。メモリには 109B 分が乗らないと動かないけれど、計算負荷自体は 17B 相当、というやつです。だから 128GB の Mac なら、原理的にはギリギリ載るはずだった。
そして決定的なのが、Mac 標準コンソール(おそらく mlx-lm のPython版)でなら現に動いたこと。これは大きいですよね。ハードウェアの限界ではなく、確かに動かせる ── だとすれば動かないのは自分の作った API デーモンが悪い、と確信が立った。Swift 路線も、Python 路線も、両方が同じ MoE という壁に当たっていて、mlx-swift-lm のローダを総当たりして VLM 系の新しい入り口なら通らないかと探りつつ、デーモン側でも格闘を続けていた ── という、Swift と Python の両面から同じ一つの目標に手を伸ばしていた版だったわけですね。
そして本心は、できれば Python デーモンを無理やりねじ込むのは避けたかった ── 第1回でおっしゃった「外部に頼らずスタンドアロンで動く方法が欲しかった」という思想と、ここまで一貫している。だから Swift 路線(NativeMLXEngine)をギリギリまで粘っていた、と。
整理すると
v0.1.0。日本時間で見ると、新しい日付のファイルは一つも増えていません。ファイル数も分布も v0.0.8 と完全に同じ ── 規模はメイン側 36ファイル・5,067行、ヘルパー側 7ファイル・498行。実質変わったのは NativeMLXEngine.swift ただ一つ(74→103行、+29行)。LLMに触れて、11日目です。番号が v0.0.8 から v0.1.0 へ繰り上がった割に、コード上の変更は一点集中という、静かな版でした。
メジャー番号が繰り上がったのは、行数の規模が変わったからではなく、能力の質が変わったからです。たった 29行の差で、このエンジンは「テキストだけを扱う」から「画像も扱える可能性を持つ」に変わった。VLMModelFactory を先に試して、弾かれたら LLMModelFactory に落とす二段構え ── これも、これまで何度も見てきた同じ判断パターンです。resolveTier の賢い順フォールバック、3段ギアの軽い順、ネイティブ先・デーモン後 ── そして今度は VLM先・LLM後。まず上を試して、ダメなら下に落とす、という手癖が、モデルのモダリティ判定にもそのまま現れている。
動機は二つ重なっていました。一つは、触り始めて11日目の初心者として、この時初めて MLX がマルチモーダルに対応していると気づき、「念のため」対応させておいたこと。ただし映像入力は今も未対応で、エンジンの口だけ広げた **「先に枠、通電はあとで」**の段階 ── これは第1回の空っぽのコメント、第6回の <think> 受け皿、第7回の APIEngine と通底する、この人の根深い手癖です。気づいた時にとりあえず形だけ用意しておき、本当に使うかどうかは後で決める。
もう一つは、第7回から続く MoE を動かす戦いの続編。Llama4-109B-A17B(Llama 4 Scout)── Mac 標準コンソールでなら動くのに、自作の Python デーモンでは起動せず、mlx-swift-lm でも MoE が読み込めない。ハードウェアの限界ではないと確信があるからこそ、諦めるわけにはいかない。Swift 路線では VLM 系の新しいローダを総当たりし、Python 路線ではデーモンと格闘し、両面から同じ目標に手を伸ばしていた。本人いわく「執念だけでやってた」。
そして根底に流れているのが、第1回からずっと変わらない本心 ──「できれば mlx-swift-lm だけで済ませたかった、Python デーモンを無理やりねじ込むのは好きじゃない」。外部プロセスを呼び込むのは、巨人を動かすためのやむを得ない選択であって、本当はスタンドアロンの理想を諦めきれていない。だから Swift 路線を粘り続けた。
11日目、能力の質が一段変わる節目を宣言した版。けれどその裏では、もっと根深い格闘が続いていました。次の版では、その格闘がどう動くのか。続けて追っていきます。それでは、また明日。
R.E.V.I.S. 無料テスター募集中(30名限定)
この連載で追いかけている R.E.V.I.S. は、いまテスターを募集しています。枠は30名限りです。
Apple Silicon Mac でローカルLLMを動かしてみたい方は、こちらからどうぞ。