第15章 相互運用の実例・導入判断・まとめ

15.1 この章で学ぶこと

この章で学ぶ内容は以下のとおりです。

表15-1: この章で学ぶこと

学ぶこと

ポイント

Mojo → NumPy の実例

Python 資産をそのまま活かしながら Mojo を使う

性能境界の設計

どこを Mojo にして、どこで Python を呼ぶかが鍵

導入判断

向く場面・向かない場面の整理

第2部 の要点まとめ

アセンブリ証拠とともに学んだ言語仕様の全体像

15.2 Mojo から NumPy を呼ぶ

Mojo と NumPy の連携は、機械学習や科学計算のワークフローで最も現実的な相互運用パターンです。 「既存の NumPy コードをそのまま使いながら、ボトルネック部分だけ Mojo で書く」という段階的な移行ができます。

基本パターン:NumPy 配列の作成と参照

# Mojo から NumPy 配列を作成して表示する基本パターン。
# Python.import_module で numpy を取り込み、np.array で配列を作る。
from std.python import Python


def main() raises:
    var np = Python.import_module("numpy")
    var array = np.array(Python.list(1, 2, 3))
    print(array)

リスト15-1: numpy_basic.mojo

出力:

[1 2 3]
表15-2: Mojo から NumPy を呼ぶ際のポイント

ポイント

説明

Python.import_module("numpy")

CPython を起動し import numpy を実行。npPythonObject

np.array(...) の速度

NumPy の C 拡張が動く。Mojo のコンパイル済みコードは介在しない

「自動高速化」の誤解

Mojo から呼んだ Python コードは CPython の世界で実行されるため、速度は変わらない

配列の中身へのアクセス

ctypes.data の生アドレスを unsafe_get_as_pointerPointer 化して直接読み書きするとコピーなし

ゼロコピーで NumPy バッファを読む

NumPy 配列の実データは連続メモリバッファに格納されています。 ctypes.data で得た生アドレスを unsafe_get_as_pointerPointer に変換すれば、Mojo 側でコピーなしに読み書きできます。

# NumPy 配列のバッファを Mojo からゼロコピーで読み書きする例。
# ctypes.data の生アドレスを Pointer に変換し、書き込みは NumPy 側に反映される。
from std.python import Python


def main() raises:
    var np = Python.import_module("numpy")
    var arr = np.zeros(4, dtype=np.float64)
    # ctypes.data は配列先頭の生アドレス(NumPy が所有するメモリ)。
    var ptr = arr.ctypes.data.unsafe_get_as_pointer[DType.float64]()
    # Mojo から直接書き込む。コピーせず NumPy 配列にそのまま反映される。
    ptr[unsafe_offset=0] = 1.0
    ptr[unsafe_offset=1] = 2.0
    ptr[unsafe_offset=2] = 3.0
    ptr[unsafe_offset=3] = 4.0
    print(arr)  # [1. 2. 3. 4.]

リスト15-2: numpy_zero_copy.mojo

データのコピーは発生しません。 NumPy と Mojo が同じメモリ領域を共有します。

段階導入のパターン

「Python を捨てずに、ボトルネックの部分だけ Mojo で書き直す」が現実的な導入パターンです。

../_images/adoption_pattern.jpg

図15-2: 段階導入のパターン

ポイントは 境界の数を最小化する ことです。 「データを NumPy で用意 → ポインタを Mojo に渡す → 計算結果を NumPy に戻す」という 1 往復に絞るほど、GIL 取得と型変換のコストが減ります。

15.3 導入判断

Mojo を実際のプロジェクトに導入するかどうかを判断するための基準を整理します。 「Mojo は万能ではない」という前提で、向く場面と向かない場面を見極めます。

向く場面・向かない場面

表15-3a: 導入判断マトリクス

向く場面

向かない場面

最初に試す場所

Python で性能限界が見えている数値処理

一般 Web アプリ・スクリプト全体の全面移植

uv run mojo で Quickstart を動かす

SIMD・GPU・カーネル寄りのコード

Python の動的な感覚をそのまま期待する用途

NumPy interop でデータ受け渡しを試す

CPU / GPU / AI accelerator を見据える処理

すぐに強い安定性が必要なプロダクション

ホットパス 1 本だけ Mojo 化して速度比較

Python 資産を残したまま部分高速化

多人数チームでの全面移行(エコシステム成熟待ち)

本書 第3部 の MicroGPT 実装を写経する

導入を決める前のチェックリスト

Mojo 化を検討する前に、以下を確認します。

  1. プロファイリングで本当にそこがボトルネックか?cProfilepy-spy 等でホットパスを特定してから検討する。「なんとなく遅い」でいきなり Mojo に移行しない。

  2. NumPy / PyTorch で解決できないか?:SIMD 最適化や GPU 転送は NumPy や PyTorch でも部分的に解決できる。Mojo が必要になるのは「既存ライブラリでは対応できない自前カーネル」が必要な場合。

  3. チームが所有権、comptime、trait を学習するコストを払えるか?:Python に慣れたチームには学習コストがある。1 人の PoC から始めるのが現実的。

  4. エコシステムの成熟を待てるか?:1.0.0 で言語仕様と標準ライブラリの主要部分は安定版になったが、サードパーティ製ライブラリやツールは Python ほど揃っていない。周辺エコシステムの充実を待つか、特定用途に限定する。

判断フロー

../_images/adoption_decision_flow.jpg

図15-3: Mojo 導入の判断フロー

コラム: 1.0 時点での現実的な評価

Mojo 1.0.0(本書執筆時点の最新安定版)は言語仕様と標準ライブラリの主要部分が安定版になりましたが、サードパーティ製ライブラリやツールを含むエコシステムは Python と比べてまだ小さいです。 「今すぐプロダクション全体を Mojo に移行する」よりも、性能が課題になっている部分を限定的に試す PoC から始めるのが現実的です。

一方で、uv run mojo で環境が整う手軽さと、Python 相互運用の充実度は今でも強みです。

15.4 第2部 の要点:アセンブリで学んだこと

第2部 では「言語仕様の説明」だけでなく、アセンブリを直接確認することで動作を証拠として示しました。 「コンパイラが何をしているか」を目で見て確かめることは、「なぜこの言語機能が速いのか」を理解する手がかりになります。

確認したアセンブリ証拠の一覧

表15-4: 第2部 のアセンブリ証拠まとめ

言語機能

アセンブリの証拠

何を示すか

var n: Int = 42(第7章 7.2〜7.3)

movq $0x2a, 0xf8(%rsp)

スタック上に 8 バイト。コンパイル時にオフセット確定

SIMD 加算(第8章 8.2.3)

addpd %xmm0, %xmm2

1 命令で 2 つの Float64 を同時加算

mut self(第9章 9.3)

Counter::increment(Counter&)

&(参照)がシンボルに現れる

self(第9章 9.3)

Counter::get(Counter)

参照なし(コピーまたは read)

__deinit__(第10章 10.10)

callq "Resource::__deinit__(Resource$)"

スコープ終了時にコンパイラが自動挿入

comptime if(第11章 11.2)

movq $0xa のみ(else 節なし)

選ばれなかった枝はバイナリに存在しない

comptime for(第11章 11.3)

3 つの callq print が直列

ループ命令なし。完全に展開される

width=8(第11章 11.4)

シンボル名に width=8 が刻まれる

コンパイル時 parameter の specialization

アセンブリが教えてくれること

各証拠が示す「設計上の意図」を整理します。

「ゼロコスト」の意味comptime if で選ばれなかった枝はバイナリに存在しません。 「実行時に分岐する」ではなく「使わないコードは生成されない」のが Mojo の comptime です。

「自動管理」の意味__deinit__ の呼び出しはコンパイラが自動挿入します。 プログラマが free() を書かなくても、スコープを抜けた時点で確実に解放されます。 これは「実行時の GC が回収する」のではなく「コンパイル時にデストラクタ呼び出し位置が決まる」仕組みです。

SIMD の意味addpd %xmm0, %xmm2 は 2 つの Float64 を同時に加算する 1 命令です。 Python のループで 1 要素ずつ処理するのとは異なる実行モデルです。

15.5 第2部 → 第3部 への橋渡し

第3部 では、第2部 で学んだ言語機能を使って MicroGPT(最小限の GPT 実装) を Mojo で書きます。 MicroGPT は GPT-2 相当のアーキテクチャを簡略化したもので、「トークン埋め込み → Transformer 層 → 出力確率」という推論パスを Mojo で実装します。

第2部 の知識がどこに使われるか

表15-5: 第2部 → 第3部 への橋渡し

第2部 で学んだこと

第3部 での使われ方

SIMD[DType.float32, N]

行列積・Softmax・ReLU・LayerNorm の SIMD ベクトル演算

struct + mut self

TransformerMultiHeadAttentionFFN 層の型定義

comptime parameter

シーケンス長・埋め込み次元・ヘッド数を定数化してループ展開

Pointer / LayoutTensor

重みテンソルのメモリ配置とカーネルへの引き渡し

Python interop

tiktoken(トークナイザー)と .safetensors 形式の重みファイル読み込み

OwnedPointer / ArcPointer

層ごとの重みバッファのライフタイム管理

MicroGPT の全体像

../_images/microgpt_overview.jpg

図15-5: MicroGPT の全体像

第2部 をもう一度振り返る視点

第3部 を読むときに、次の問いを持つと理解が深まります。

  • struct__init__ は何を確保しているか? → 重みバッファの alloc

  • mut self を持つメソッドは何を変更しているか? → 中間活性化テンソルの書き換え

  • comptime parameter はどこにあるか?embed_dimn_headsseq_len

  • Pointer はどこに現れるか? → テンソルの生メモリアクセス

  • Python interop の境界はどこか? → 起動時の重み読み込みとトークン変換のみ

15.6 まとめ

本章で扱った内容を以下にまとめます。

表15-6: まとめ

要素

押さえるべきこと

Mojo の本質

Python に近い入口を持ちながら、ownership / comptime / SIMD が中心

Python interop の設計

ホットパスは Mojo に閉じ込め、境界をまたぐ回数を最小化

導入の現実

全面移行より部分 PoC が現実的(エコシステム発展中)

アセンブリが示すこと

comptime if で消える枝・addpd の SIMD・__deinit__ の自動挿入

次の一歩

第3部 MicroGPT で言語機能を実装レベルで体験する