さとまたwiki
🔀

MoEアーキテクチャ大解剖

「4GBで500Bを動かす」鍵となるMixture of Expertsを、メモリ割り当てからソースコードまで完全分解

Mixture of Experts 疎活性化 Mixtral DeepSeek V3 メモリ最適化

🔀 MoEとは何か — 疎活性化の革命

MoE(Mixture of Experts, 専門家の混合)とは、「モデル全体のごく一部のパラメータだけを、各トークンで活性化させる」アーキテクチャだ。従来のDenseモデルが全パラメータを毎回使うのに対し、MoEは入力ごとに最適な数個の「専門家(Expert)」だけを選んで走らせる。この「疎活性化(Sparse Activation)」こそが、MoEの核心だ。

  • ・DeepSeek V3: 671B総パラメータ、アクティブ37B(5.5%のみ使用
  • ・Mixtral 8x7B: 実質47B総パラメータ、アクティブ13B(28%のみ使用
  • ・GPT-4(推定): 1.76T総パラメータ、アクティブ約280B(16%のみ使用

💾 なぜMoEがメモリ問題を解くのか

UIAPduinoページで語った「500Bを4GBで」の実現に直結する重要セクションだ。

項目Dense 500BMoE 500B (Top-2 / 8 experts)
総パラメータ500B500B
1トークンあたりの計算量500B × 2 FLOPS125B × 2 FLOPS(1/4)
メモリ常駐量(最小)500B 全部Routing層 + Top-K Expert分のみ
理論上のSSDオフロード可能性困難(全部同時に使う)容易(未使用Expertは捨てられる)
Q2量子化後のVRAM最小要求125GB30〜40GB → さらにストリーミング可

Denseモデルでは全重みを毎トークンで使うため、SSDオフロードは推論速度が壊滅的に遅くなる。MoEは「このトークンではこのExpertしか使わない」と事前に分かるため、使わないExpertは物理メモリから外して良い。これがSSDストリーミング推論と相性が良い本質的理由だ。

🧠 MoEの5つの部品

① Router (Gate)

各トークンをどのExpertに送るかを決める軽量な分類器。入力次元 × Expert数の1層線形層。Mixtralで約32KB、ほぼ無視できるサイズ。

② Experts (FFN群)

各Expertは通常のTransformerのFFN(2層MLP)。8〜256個を保持。ここがモデルサイズの大部分。

③ Top-K Selection

Routerの出力から上位K個(Mixtral=2、DeepSeek V3=8)のExpertを選択。torch.topk 一発。

④ Weighted Aggregation

選ばれたExpertの出力を、Routerのsoftmax重みで加重平均。

⑤ Load Balancing Loss

学習時のみ使う補助損失。Expertが偏って使われるのを防ぐ「死にExpert」対策。

📐 Dense vs MoE の構造比較

Dense Transformer Block (Python)

class DenseBlock(nn.Module):
    def __init__(self, dim, ffn_dim):
        super().__init__()
        self.attn = Attention(dim)
        self.ffn = FeedForward(dim, ffn_dim)  # 全トークン共通

    def forward(self, x):
        x = x + self.attn(x)
        x = x + self.ffn(x)  # 毎回全パラメータ使用
        return x

MoE Transformer Block (Python)

class MoEBlock(nn.Module):
    def __init__(self, dim, ffn_dim, num_experts=8, top_k=2):
        super().__init__()
        self.attn = Attention(dim)
        self.moe = MixtureOfExperts(
            dim, ffn_dim,
            num_experts=num_experts,
            top_k=top_k
        )

    def forward(self, x):
        x = x + self.attn(x)
        x = x + self.moe(x)  # Top-K個のExpertだけ使用
        return x

🔍 ソースコード解剖:Mixtral 8x7B

Mixtralは、Mistral AI が公開した最もシンプルで参照しやすいMoEの公式実装だ。公式リポジトリ mistralai/mistral-src は約1500行。以下はその実装の骨格を、初学者向けに整理したもの。

import torch
import torch.nn as nn
import torch.nn.functional as F

class MixtralMoE(nn.Module):
    def __init__(self, hidden_dim=4096, ffn_dim=14336,
                 num_experts=8, top_k=2):
        super().__init__()
        self.num_experts = num_experts
        self.top_k = top_k

        # ① Router: 軽量な1層線形層(4096→8 で32KBのみ)
        self.gate = nn.Linear(hidden_dim, num_experts, bias=False)

        # ② Experts: 独立したFFNを8個保持(ここがメモリの大部分)
        self.experts = nn.ModuleList([
            FeedForward(hidden_dim, ffn_dim)
            for _ in range(num_experts)
        ])

    def forward(self, x):
        # x: [batch, seq_len, hidden_dim]
        batch, seq, dim = x.shape
        x_flat = x.view(-1, dim)  # [batch*seq, dim]

        # ③ Router: 各トークンの「どのExpertに行くか」のスコア計算
        router_logits = self.gate(x_flat)  # [batch*seq, num_experts]

        # ④ Top-K選択: 上位2個のExpertだけ残す
        routing_weights, selected = torch.topk(
            router_logits, self.top_k, dim=-1
        )
        routing_weights = F.softmax(routing_weights, dim=-1)

        # ⑤ 選ばれたExpertだけ実行
        output = torch.zeros_like(x_flat)
        for expert_id in range(self.num_experts):
            # このExpertが選ばれたトークンのインデックス
            mask = (selected == expert_id).any(dim=-1)
            if not mask.any():
                continue  # このExpertは使われない → メモリから外せる

            tokens = x_flat[mask]
            expert_out = self.experts[expert_id](tokens)

            # ⑥ Routerの重みで加重
            weight = routing_weights[mask].sum(dim=-1, keepdim=True)
            output[mask] += weight * expert_out

        return output.view(batch, seq, dim)
  • Routerは軽量。Mixtralで4096×8=32KBのパラメータのみ
  • Expertsが本体。8個×約180MB = 約1.4GB/層(FP16)
  • 各トークンに対し「どのExpertが得意か」のlogitを算出
  • torch.topk でスパース化。Top-2なら2/8=25%のExpertだけ使う
  • ここが省メモリの核if not mask.any(): continue でそのExpertを丸ごとスキップできる。SSDからロードしていない状態でも動く
  • Routerのsoftmax確率で出力を加重平均

🧮 Routerの数学とLoad Balance

Routerが雑に訓練されると「特定のExpertばかりに仕事が集中する」問題が起きる(死にExpert)。これを防ぐのがLoad Balancing Lossだ。MoEモデルの学習では、通常のCross Entropy Lossに加えてこの補助損失を混ぜる。

def load_balance_loss(router_probs, selected_experts, num_experts):
    """
    router_probs: [batch*seq, num_experts]  Softmax後のRouter確率
    selected_experts: [batch*seq, top_k]     選ばれたExpert ID
    """
    # f_i: Expert iが選ばれた割合(one-hotの平均)
    mask = F.one_hot(selected_experts, num_experts).float()
    f_i = mask.mean(dim=(0, 1))  # [num_experts]

    # P_i: Expert iへのルーティング確率の平均
    P_i = router_probs.mean(dim=0)  # [num_experts]

    # f_i と P_i の両方が均等なほど損失が小さくなる
    return num_experts * (f_i * P_i).sum()

この補助損失により、Routerは「全Expertを平均的に使うように」と逆圧力を受け、Expert間の専門性が自然に分化していく。Mixtralでは router_aux_loss_coef=0.02 程度の重みで混ぜるのが標準。

💾 メモリ割り当ての実際

前ページで掲げた「500Bを4GBで動かす」という挑戦が、MoEによって現実的になる。以下はMixtral 8x7B を例にした、設定別のメモリ要求量。

設定メモリ使用量備考
全Expert常駐 FP16約90GB普通のMixtralデプロイ、GPU 2〜4台要る
全Expert常駐 Q4約24GBllama.cpp Q4_K_M デフォルト
全Expert常駐 Q2約12GBアグレッシブ量子化、精度は若干低下
Top-K Expertのみロード Q2約3.5GBSSDストリーミング推論
+ KV Cache圧縮 (GQA/MLA)約3.8GB4GB制約内に収まる
// エキスパートキャッシュ(LRU方式)の擬似コード
struct ExpertCache {
    std::unordered_map<int, ExpertWeights*> hot;
    std::list<int> lru;
    size_t max_hot_experts = 4;  // 4GB制約内で保持できる数

    ExpertWeights* get(int expert_id) {
        if (hot.count(expert_id)) {
            // ヒット: LRU更新
            lru.remove(expert_id);
            lru.push_front(expert_id);
            return hot[expert_id];
        }
        // ミス: SSDからmmapでロード
        if (hot.size() >= max_hot_experts) {
            int victim = lru.back();
            lru.pop_back();
            munmap(hot[victim]->data, hot[victim]->size);
            hot.erase(victim);
        }
        auto* w = load_from_ssd_mmap(expert_id);
        hot[expert_id] = w;
        lru.push_front(expert_id);
        return w;
    }
};

現実のPCIe 5.0 NVMeは14GB/s の帯域を持つ。Expert 1個(約180MB FP16 / 45MB Q2)のロード時間は3〜13ms。トークン生成が100ms間隔なら、ミス時のペナルティは許容できる。ホットExpertが十分キャッシュされれば、ほとんどのトークンでSSDアクセスは発生しない。

📊 実モデル比較:Mixtral / DeepSeek V3 / Qwen MoE

モデル総パラメータExpert数Top-Kアクティブ特徴
Mixtral 8x7B47B8213B最もシンプル、公式ソース公開
Mixtral 8x22B141B8239B大型版、GPT-4 Turbo級
DeepSeek V2236B160 + 2共有621B共有Expert導入、MLA採用
DeepSeek V3671B256 + 1共有837BMTP、FP8学習、現最強級
Qwen1.5-MoE-A2.7B14B6042.7B小型MoE、Expertが多い
OLMoE 7B-1B7B6481BAllenAI完全オープン
Grok-1314B8286BxAI、巨大版Mixtral型

DeepSeek V3の「Expert 256個中Top-8」は特に興味深い。細分化が進むほどRouterの選択自由度が上がり、Expert間の専門性分化が明確になる。SSDストリーミング視点では、Expertが細かいほど「必要なものだけロード」の粒度が細かくなり有利。

🐧 最初に読むべきLLM — LLM界のUbuntu

「LinuxでいうUbuntuのように、最初に触るのに単純でいい」——この問いへの答えは明確だ。LLMのソースコードを初めて読むなら、Andrej Karpathy の nanoGPT 一択。Ubuntu と同じく、入門者が迷わないよう設計された「デフォルトの選択肢」だ。

結論:最初は nanoGPT(300行、1ファイル、PyTorchのみ)

全体を1日で読める。Transformer の要素が全て入っている。MoE は入っていないが、MoEを理解するにはまずDense Transformerを読む必要がある。

📚 候補比較表

LLM規模行数目安言語読みやすさ何が学べるかLinux例え
nanoGPT (Karpathy)GPT-2 124M300行Python★★★★★Transformer基礎、1ファイルで完結Ubuntu Desktop
llama2.c (Karpathy)Llama 2 7B700行C★★★★★純C言語での推論、メモリ管理Raspberry Pi OS
minGPT (Karpathy)GPT系800行Python★★★★nanoGPTの教育的拡張版Ubuntu LTS
Mistral-src 公式Mistral 7B / Mixtral1,500行Python★★★★MoE公式リファレンスDebian
OLMoE (AllenAI)7B MoE中規模Python★★★★学習コードまで完全オープンFedora
llama.cpp全般10万行超C/C++★★★量子化、mmap、プロダクション推論Arch Linux
Transformers (HF)全般100万行超Python★★全モデル網羅、抽象度高いRed Hat Enterprise
DeepSeek V3公式671B MoE数万行Python★★最先端MoE、MLA、MTPGentoo
Megatron-LM (NVIDIA)学習向け膨大Pythonテンソル並列、分散学習LFS

💡 Claudeのおすすめ順序

Step 1: nanoGPT(1週間)

まずこれ。train.pymodel.py の2ファイルだけで完結する。TransformerのAttention・FFN・位置埋め込みを、100行以内の関数として目で追える。

Step 2: llama2.c(2〜3日)

C言語で推論のみ。malloc でメモリがどう確保され、行列演算がどう走るかを肉眼で見る。「メモリ割り当てから理解したい」派には神ファイル。

Step 3: Mistral-src(1週間)

公式の最小MoE実装。nanoGPT と比べて MoE の追加要素だけ差分で理解できる。ここで Router・Top-K・Expert を完全に把握する。

Step 4: OLMoE or DeepSeek V3 リファレンス(1ヶ月〜)

本気の現代MoE。共有Expert、細粒度ルーティング、MLA、補助損失なしのLB(DeepSeek独自)まで踏み込む。

🗺️ 学習ロードマップ

Phase 1: Transformer 完全理解(2週間)

nanoGPTを写経→学習→推論。Attentionの数式を手書きで追う。"The Annotated Transformer" を併読。

Phase 2: C言語で推論を実装(1週間)

llama2.cを1行ずつ読み、自分で別モデルを動かす。mmap(2) の man を読む。

Phase 3: MoE 構造の把握(2週間)

Mistral-src を読解。Mixtralのweightをダウンロードし、自作Routerで推論を再現。

Phase 4: 4GB推論エンジン自作(3ヶ月〜)

UIAPduinoページのPhase 1と合流。llama.cpp を fork して、Expert キャッシュを実装。

🎯 まとめ:MoEは「計算量を減らすのではなく、メモリを減らす」技術

MoEの本質は、単に「計算量が4分の1になる」ことではない。それ以上に重要なのは、「メモリに何を常駐させるかを選べる」という性質だ。Denseモデルでは全重みが常時必要だが、MoEでは「今このトークンで必要なExpertだけ」を選別できる。この性質が、SSDストリーミング推論・エッジデバイスでの大規模モデル運用を可能にする。

「4GBのメモリで500Bのモデルを動かす」という挑戦は、MoEなしには成立しない。MoEは、個人がLLM基盤を持つための鍵だ。

← 関連: UIAPduino — 290円マイコンボードの個人量産