はじめに
これまでの記事では、Elixir と Livebook を使って LLM の仕組みを学び、Elixir 製の LLM 推論エンジン Llamex の内部を見てきました
通常の Llamex は、純 Elixir のリスト演算だけでも LLM を推論できます
それなら、同じ Elixir コードをマイコン向けの小さな Erlang VM へ持ち込めるのでしょうか
Llamex の実験的な派生版である Luna版 は、この問いを掘り下げています
LLM 推論エンジンを小さな実行環境へ持ち込むために、依存ゼロの境界をどう作るか?
この記事では、次の順番で Luna 版を見ていきます
- AtomVM と通常の BEAM の違いを整理する
- Nx をインストールせずに Llamex が動くことを確かめる
-
Backend.AtomVMが表すポータブル境界を見る -
Portableファサードを通常の BEAM 上で動かす - 最小 smoke が何を検証し、何を検証しないか整理する
- PackBEAM の
--pruneで到達可能なモジュールだけを残す
対象読者は次のような方です。
- Elixir はある程度書けるが、AtomVM は使ったことがない
- 組込 Elixir や、依存の少ないアプリケーション設計に興味がある
- 「純 Elixir ならマイコンでも動く」という説明を、もう一段具体的に理解したい
- コード、表、構成図を見ながら仕組みを追いたい
ノートブック
この記事で使うノートブックは GitHub で公開しています
以下のバッジから Livebook で直接開けます
事前準備
Livebook のインストール方法は、公式サイトを参照してください
Luna 版は Elixir 1.19 以上を要求します
ノートブックを開くと、先頭の Mix.install/1 が必要なライブラリをインストールします
Mix.install([
{:llamex,
github: "piacerex/llamex-gpt-5.6-luna-xhigh",
ref: "8dd68427239425d3f21a6e010788c1d9db98dc61"},
{:kino, "~> 0.15"},
{:kino_vega_lite, "~> 0.1"},
{:req, "~> 0.5"}
])
依存一覧に Nx がないことが、今回のポイントです
再現性のため、ノートブックでは Llamex のコミット、モデルのリビジョン、SHA-256 を固定しています
また、検証する場所を最初に区別しておきます
| 検証 | 実行する場所 |
|---|---|
Backend.AtomVM で GGUF をロード・生成 |
Livebookが動く通常のBEAM |
| 手作りモデルの最小経路 | Livebook上のBEAMで再現 |
.avm の作成と実行 |
別途用意したAtomVM / PackBEAM環境 |
| ESP32などのマイコン実機 | このノートブックの範囲外 |
Livebook で Backend.AtomVM を選んだだけで、ノートブック自体が AtomVM 上で動くわけではありません
AtomVM とは
AtomVM は、マイコンなどの小さな環境で Erlang VM の機能を動かすための実装です
通常の BEAM とマイコン上の AtomVM では、前提が異なります
| 観点 | 通常のBEAM | マイコン上のAtomVM |
|---|---|---|
| メモリ | PCやサーバーのGB単位 | KB〜MB単位を意識する |
| コード | 必要な依存を広く利用できる | バンドルするモジュールを絞る |
| 数値計算 | Nx / EXLA / NIFを利用できる | 純Elixirで到達可能な経路が中心 |
| ファイル | OSのファイルシステムを使える | ボードとAtomVMの機能に依存する |
Llamex の Backend.List は、リスト演算だけで書かれた純 Elixir のバックエンドです
つまり数値計算にはポータブルな経路があります
しかし、純 Elixir で書かれているだけでは十分ではありません
- 利用できない標準ライブラリを呼んでいないか
- Nx や NIF に依存する経路が混ざっていないか
- 不要なモジュールをフラッシュへ入れていないか
- モデルを展開したときにメモリへ収まるか
Luna 版は、これらのうち特に コードと依存の境界 を明確にします
Nx なしでも動くことを確かめる
まず、現在の実行環境を表にします
Kino.DataTable.new([
%{
確認: "Nxモジュールは存在するか",
結果: inspect(Code.ensure_loaded?(Nx))
},
%{
確認: "Backend.Nxモジュールは存在するか",
結果: inspect(Code.ensure_loaded?(Llamex.Backend.Nx))
},
%{
確認: "Backend.AtomVMモジュールは存在するか",
結果: inspect(Code.ensure_loaded?(Llamex.Backend.AtomVM))
}
])
結果は少し不思議です。
| 確認 | 結果 |
|---|---|
| Nxモジュール | 存在しない |
Backend.Nx |
モジュール自体は存在する |
Backend.List / Backend.AtomVM
|
存在する |
Elixir では、ほかのモジュールへの呼び出しは実行時に解決されます
そのため、Nx を呼ぶコードがコンパイル済みモジュールに含まれていても、その経路を実行しなければ Nx は要求されません
逆に、Backend.Nx.from_list/1 を実行すると、その時点で Nx がないため失敗します
通常の BEAM では「使わなければよい」で済みますが、組込では不要なモジュールを入れること自体がフラッシュ容量の無駄になります
そこで必要になるのが、どのモジュールをバンドルへ含めるかという境界です
依存そのものを空にできるビルド
Luna 版では Nx と EXLA が optional 依存です
さらに、AtomVM 向けの環境では依存リスト自体を空にします
defp deps do
if portable_build?() do
[]
else
[
{:nx, "~> 0.12.1", optional: true},
{:exla, "~> 0.12.0", optional: true}
]
end
end
defp portable_build? do
Mix.env() == :atomvm or
System.get_env("LLAMEX_PORTABLE") in ["1", "true"]
end
MIX_ENV=atomvm または LLAMEX_PORTABLE=1 で、Nx が「使われない」だけではなく「依存へ入りようがない」構成になります
なお、Livebook で Nx が入っていないのは Mix.install/1 の動作によるものです
依存パッケージ側の optional 依存は、ノートブック側で明示しない限りインストールされません
Backend.AtomVM は計算ではなく境界を表す
Luna 版の中心が Llamex.Backend.AtomVM です
名前を見ると AtomVM 専用の数値計算を実装していそうですが、実際には全22演算を Llamex.Backend.List へ委譲します
defmodule Llamex.Backend.AtomVM do
@behaviour Llamex.Backend
alias Llamex.Backend.List
defdelegate dot(left, right), to: List
defdelegate matvec(rows, vector), to: List
defdelegate rms_norm(input, weight, epsilon), to: List
# 残りの演算も List へ委譲
end
同じ計算なら、なぜ別モジュールにするのでしょうか
理由は、AtomVM のバンドルへ入れるモジュールの入口を固定するためです
Backend.AtomVM は新しい計算方法ではありません
「この先は依存ゼロの経路だけ」という設計上の境界です
この入口があることで、次の関係が明確になります
- アプリは AtomVM 向けの入口として常に同じモジュールを参照する
- その先は
Backend.Listと純 Elixir のレイヤー実装だけになる - 将来
Backend.Listが変わっても、AtomVM 向けの契約を別モジュールで守れる - PackBEAM が参照関係をたどって不要なモジュールを落とせる
制約をコメントだけでなく、モジュール構造で表現しているわけです
Portable ファサードを BEAM 上で試す
Llamex.Portable は、ロードから生成まで AtomVM バックエンドを既定にする窓口です
def load(path, opts \\ []) do
model = Llamex.GGUF.ModelLoader.load(path, opts)
Llamex.prepare_model(model, Llamex.Backend.AtomVM)
end
def generate(model, prompt, opts) do
Llamex.generate(
model,
prompt,
Map.put(opts, :backend, Llamex.Backend.AtomVM)
)
end
ノートブックでは、前回と同じ stories260K.gguf を読み込みます
prepared = Llamex.Portable.load(model_path)
result =
Llamex.Portable.generate(prepared, "Once upon a time", %{
max_new_tokens: 10,
sampler: :greedy
})
通常版の Backend.List と同じ生成結果になります
ここで確認できるのは、通常の BEAM 上で Backend.AtomVM を選んでも、依存ゼロの純 Elixir 経路が同じ結果を返すことです
このセルだけで「マイコン上で GGUF モデルが動いた」とは言えません
Portable.load/2 はファイル読み込みも使うため、実機では利用できる AtomVM API、モデルの置き場所、メモリ量を別途検討する必要があります
ファイルサイズと実行時メモリは別物
ノートブックでは、モデルの大きさを3つの指標で比較します
| 指標 | 意味 | 注意点 |
|---|---|---|
| GGUFファイルサイズ | ストレージ上の容量 | 実行時メモリとは一致しない |
| F32数値の理論サイズ | 値だけをF32で持つ下限 | リスト構造の管理領域を含まない |
| BEAM全体の実測増加 | ロード前後のメモリ差 | GCや他プロセスにも影響される |
実測値はセッションによって変わるため、Livebook では実行のたびに計測して棒グラフにします
Elixir の数値リストは、各数値だけでなく、リストをつなぐための管理領域も必要です
そのため、約1.1MBの GGUF であっても、読み込み後のメモリはファイルサイズより大きくなります
これは Luna のポータビリティだけでは解決しない問題です
実用的なモデルを小さな環境へ載せるには、量子化ペイロードを展開せず保持する工夫も必要になります
最小 smoke が検証する範囲
Luna 版には examples/atomvm_smoke.ex があります
語彙3、埋め込み2次元の手作りモデルを使い、1トークン推論の最小経路を確認します
tiny_model =
Llamex.new_model(%{
config: %{vocab_size: 3, embedding_size: 2},
token_embeddings: %{
0 => [1.0, 0.0],
1 => [0.0, 1.0],
2 => [0.8, 0.2]
}
})
prepared =
Llamex.prepare_model(
tiny_model,
Llamex.Backend.AtomVM
)
context = Llamex.new_context(prepared)
{_context_after, next_token} = Llamex.next_token(context, 0)
出力層がないため、ロジットには埋め込み同士の内積を使います
トークン0の埋め込み [1.0, 0.0] と最も内積が大きいのは自分自身なので、次のトークンは 0 です
このモデルには Transformer レイヤーがありません
したがって、smoke が通ったときに確認できる範囲は限定されています
| 計算経路 | このsmokeで検証するか |
|---|---|
| 埋め込みの参照 | ✓ |
| 埋め込み同士の内積によるロジット | ✓ |
| argmax | ✓ |
| Attention / RoPE | — |
| RMS正規化 / FFN | — |
| KVキャッシュ | — |
Livebook のセルは、同じ最小経路が BEAM 上の Backend.AtomVM で期待値を返すことを確認します
同じコードを generic UNIX 版 AtomVM で動かせば、依存ゼロの最小推論経路と argmax が AtomVM 上でも動くことを確認できます
Transformer の全経路を検証するには、少なくとも1層を持つ別の smoke が必要です
モジュールのフットプリントを見る
組込では、コードの容量もフラッシュ使用量へ直接影響します
ノートブックでは、コンパイル済み Llamex の .beam ファイルを分類します
| 分類 | 例 | AtomVMバンドルでの扱い |
|---|---|---|
| ポータブル経路 |
Backend.AtomVM、Backend.List、レイヤー実装 |
必要 |
| Nx経路 |
Backend.Nx、Backend.NxEXLA
|
到達しなければ除外 |
| 開発用タスク | Mix.Tasks.* |
実行時には不要 |
ここで計測する .beam 合計は、prune 前の上限です
実際の .avm サイズは、次の PackBEAM 実行後に別途測る必要があります
PackBEAM で到達可能なモジュールだけを残す
AtomVM では、コンパイル済みモジュールを .avm バンドルへまとめます
Luna 版の Mix タスクは、次の3段階を自動化しています
手動で実行する場合のコマンドは次の形です
elixirc \
-pa _build/dev/lib/llamex/ebin \
-o workspace \
examples/atomvm_smoke.ex
packbeam create --prune \
--start Elixir.Llamex.Examples.AtomVMSmoke \
llamex_atomvm_smoke.avm \
workspace/Elixir.Llamex.Examples.AtomVMSmoke.beam \
_build/dev/lib/llamex/ebin/*.beam
atomvm llamex_atomvm_smoke.avm atomvmlib.avm
--prune は、エントリモジュールから参照をたどり、到達できるモジュールだけを残します
アプリが Backend.AtomVM だけを入口にすれば、そこから参照されない Nx 系や Mix タスクはバンドルから落ちます
なお、Livebook ではこのコマンドを実行しません
実行には AtomVM と PackBEAM が必要であり、atomvm と atomvmlib.avm のビルド、さらに BEAM を作る OTP / Elixir と AtomVM の対応状況を合わせる必要があります
ESP32 などの実機では、対象ボード用の AtomVM ファームウェア、利用可能な標準モジュール、フラッシュへの書き込み手順も確認します
Luna版の現在地
ここまでの確認結果を、できていることと今後の課題に分けます
| 観点 | 確認できたこと | 今後必要なこと |
|---|---|---|
| 依存 | Nxなしでコンパイル・BEAM実行できる | 対象AtomVM環境との継続的な互換性確認 |
| 実行境界 |
Backend.AtomVM から純Elixir経路だけへ到達する |
Transformer全経路を含むsmoke |
| バンドル |
--prune で不要モジュールを除外する手順がある |
実際の.avmサイズと実機フラッシュの確認 |
| モデル | 260K GGUFをBEAM上のポータブル経路で生成できる | 実機でのモデル配置・ファイルI/O・メモリ対策 |
Luna 版は、マイコン上で実用的な LLM がすでに動く完成形ではありません
まず、依存ゼロの最小経路とバンドル境界を明確にした段階です
Sol版との関係
Luna 版でコードを小さく切り出しても、F32 の Elixir リストへ展開したモデルは、組込環境のメモリへ収まりません
もう1つの派生版である Sol 版は、量子化された重みをバイナリのまま保持し、必要な1ブロックだけを復元する問題を扱います
| 派生版 | 主な問い |
|---|---|
| Luna版 | どのコードと依存を小さな実行環境へ持ち込むか |
| Sol版 | 重みをどの表現でメモリへ置くか |
この図は将来の設計方向を表したものです
現在の Sol と Luna は別々の実験的フォークであり、このノートブックで両方を統合したわけではありません
Sol 版については、別の記事で量子化の原理から15Mモデルの生成まで詳しく扱います
まとめ
この記事では、Llamex Luna 版の AtomVM ポータビリティ設計を Livebook で追いました
- AtomVM では、利用できるライブラリ、NIF、メモリ、フラッシュ容量を意識する必要がある
- Luna 版は Nx をインストールせずにコンパイル・実行できる
-
MIX_ENV=atomvmまたはLLAMEX_PORTABLE=1で依存リスト自体を空にできる -
Backend.AtomVMは全22演算をBackend.Listへ委譲する、依存ゼロの明示的な境界である -
Portableファサードを使い、BEAM 上で通常版と同じ純 Elixir 経路を確認できる - 最小smokeは埋め込み、内積、argmaxを検証するが、Transformer全経路の検証ではない
-
packbeam create --pruneは、エントリから到達可能なモジュールだけを.avmへ残す - Livebook上の確認、generic UNIX版AtomVM、マイコン実機での実行は区別する必要がある
前回は Llamex の推論パイプラインを見ました
今回は、そのコードを小さな環境へ持ち込むために、 何を実装するかだけでなく、何へ到達できるかをモジュール構造で制御する 設計を確認できました
ノートブックでは、バックエンドの能力、演算結果、メモリ増加、モジュール分類、バンドルの到達関係を表とグラフで確認できます
ぜひ、セルを順番に実行して、通常の BEAM と AtomVM の境界を追ってみてください

