AIシステムアーキテクチャ

ノートPC上で大規模モデルのデモは動いたし、結果も良好だった。しかし、それを実際の製品にしようとすると、問題が生じる。

  • モデルのパラメータ数が大きすぎて、GPU1枚に収まらない場合はどうする?

  • トレーニングに数週間かかるが、途中で障害が発生した場合、どうやってチェックポイントから再開するのか?

  • 毎日数百万のユーザーリクエストがあるが、どうやって遅延を1秒以内に保証するのか?

  • ユーザーフィードバックを収集して、モデルを継続的に進化させるにはどうすればよいか?

これらの問題は、AIシステムアーキテクチャが解決すべきものだ。

デモは「動くかどうか」に焦点を当てるが、本番システムは「安定して、効率的に、低コストで動くかどうか」に焦点を当てる。

本番グレードのAIシステムの特徴: 7x24時間の可用性、百万単位の同時実行のサポート、可観測性、拡張性、信頼性の高い災害復旧、コスト管理。


AIシステム特有の課題

従来のWebサービスと比較して、AIシステムには3つの独自の課題がある。

課題1: 不確実な出力

従来のシステムの出力は決定的だ。1+1を入力すれば、常に2を返す。

AIシステムの出力は確率的だ。同じプロンプトでも、2回生成すると結果が異なる可能性がある。

これにはいくつかの問題がある: 出力品質をどう保証するか? 効果をどう評価するか? 幻覚(ハルシネーション)にどう対処するか?

解決策は通常、出力層でのサンプリング戦略、結果の後処理フィルタリング、人手によるフィードバックループ、複数モデルの投票などだ。

課題2: レイテンシとコストのトレードオフ

AI推論には大量の計算が必要であり、遅延とコストの間に自然な矛盾があることを意味する。

高速化したい? より多くのGPUを使うとコストが急上昇する。

コストを抑えたい? キューで処理すると、ユーザーエクスペリエンスが悪くなる。

本番システムの核心は、SLA(サービスレベル契約)とコストの間のバランスを見つけることだ。

最適化の方向性一般的な技術効果
モデル圧縮量子化、プルーニング、蒸留速度が2〜4倍向上、精度はわずかに低下
推論最適化vLLM、TensorRT、FlashAttentionスループットが3〜10倍向上
アーキテクチャ設計バッチ処理、多層キャッシュリクエスト単価が50%〜80%削減

課題3: データフライホイールの構築

AIシステムは「リリースして終わり」ではない。継続的な反復が必要だ。

ユーザーが使うほどフィードバックデータが増え、モデルをより良くトレーニングでき、ユーザーはさらに使いたくなる。これがデータフライホイールだ。

しかし、フライホイールを回すのは簡単ではない。効果的なフィードバックをどう収集するか? データをどうアノテーションするか? どう継続的にトレーニングするか? 新バージョンをどう評価するか?

これらの問題に標準的な答えはないが、成功したAI製品はそれぞれ独自のフライホイール設計を持っている。


大規模トレーニングインフラストラクチャ

数千億から数兆のパラメータを持つ大規模モデルをトレーニングするには、スーパーコンピューティング基盤が必要だ。

GPUクラスタアーキテクチャ

現代のAIトレーニングクラスタは通常、数百から数千のGPUで構成される。

H100 GPUを例にとると、H100 1枚には80GBのメモリがあり、演算性能はFP8精度で1979 TFLOPSだ。

しかしGPU1枚では全く不十分だ。GPT-3のトレーニングには約355枚のV100を使用し、3か月かかった。

典型的なクラスタトポロジーは次のとおり:

階層デバイス接続方法帯域幅
単一ノード内GPU-GPUNVLink900GB/s
同一ラック内サーバー間InfiniBand400Gb/s
ラック間スイッチ間InfiniBand Fabric400Gb/s

ネットワークは分散トレーニングのボトルネックだ。通信帯域幅が不十分だと、GPU利用率が90%から30%に低下し、ほとんどの時間をデータ待ちに費やすことになる。

InfiniBand高速相互接続

通常のイーサネットは遅すぎるため、分散トレーニングではInfiniBandを使用する。

InfiniBandの特徴は、極めて低い遅延(マイクロ秒レベル)、非常に高い帯域幅、RDMA(リモートダイレクトメモリアクセス)のサポートだ。

RDMAにより、GPUが別のサーバーのGPUメモリを直接読み書きでき、OSカーネルを経由する必要がないため、速度が大幅に向上する。

ストレージシステム設計

トレーニングデータは通常TBからPBレベルであり、ストレージシステムも重要だ。

典型的な階層ストレージ設計:

  • ホットデータ: SSDまたはNVMe。現在トレーニング中のバッチを格納
  • ウォームデータ: 分散ストレージ(Ceph、Lustreなど)。完全なトレーニングセットを格納
  • コールドデータ: オブジェクトストレージ(S3など)。履歴データとバックアップを格納

フォールトトレランスとチェックポイント

トレーニングに数週間かかるが、その間にGPUが1枚故障したらどうする? 最初からやり直すのはあまりにも無駄だ。

解決策はチェックポイント(Checkpointing)定期的にモデルの状態をディスクに保存し、エラーが発生したら最新のチェックポイントから復元する。

ただし、チェックポイントにもコストがかかる。1回の保存に数分かかり、数十GBの容量を占有する可能性がある。

一般的な戦略は、数百ステップごとに保存し、最近の数個のチェックポイントを保持して、古いものは自動的に削除するというものだ。


分散トレーニング戦略

1枚のGPUには大規模モデルを収容できないため、トレーニングタスクを複数のGPUに分割する必要がある。

主に3つの並列戦略がある。データ並列、テンソル並列、パイプライン並列で、これらを組み合わせたものが3D並列だ。

3D 并行策略示意图

データ並列処理(Data Parallelism)

最も簡単で最も一般的な戦略だ。各GPUに完全なモデルを配置し、異なるデータを処理する。

例えば8枚のGPUがあり、バッチサイズが1024の場合、各GPUは128件のデータを処理する。

順伝播は各GPUで独立に計算し、逆伝播後に勾配を集約して平均を取り、モデルを更新する。

データ並列の問題は、メモリが依然としてボトルネックであることだ。モデルが大きすぎてGPU1枚に収まらない場合、データ並列は役に立たない。

ZeROメモリ最適化

ZeRO(Zero Redundancy Optimizer)はデータ並列の拡張版であり、GPUメモリをさらに節約できる。

通常のデータ並列では、各GPUが完全なモデルパラメータ、勾配、オプティマイザ状態を格納する。これは非常に冗長だ。

ZeROの考え方は、これらの状態を異なるGPUに分割し、必要なときに通信するというものだ。

ZeROステージ分割する内容メモリ節約量
ZeRO-1オプティマイザ状態約4倍
ZeRO-2オプティマイザ状態 + 勾配約8倍
ZeRO-3オプティマイザ状態 + 勾配 + パラメータGPU数に比例

DeepSpeedでZeROを設定するのは簡単だ:

{
  "train_batch_size": 1024,
  "train_micro_batch_size_per_gpu": 16,
  "optimizer": {
    "type": "Adam",
    "params": {
      "lr": 0.0001,
      "betas": [0.9, 0.95],
      "eps": 1e-8,
      "weight_decay": 0.01
    }
  },
  "zero_optimization": {
    "stage": 3,
    "allgather_partitions": true,
    "allgather_bucket_size": 2e8,
    "overlap_comm": true,
    "reduce_scatter": true,
    "reduce_bucket_size": 2e8,
    "contiguous_gradients": true,
    "stage3_prefetch_bucket_size": 1e8,
    "stage3_param_persistence_threshold": 1e5,
    "stage3_max_live_parameters": 1e9,
    "stage3_max_reuse_distance": 1e9
  },
  "gradient_clipping": 1.0,
  "fp16": {
    "enabled": true,
    "loss_scale": 0,
    "loss_scale_window": 1000,
    "initial_scale_power": 16,
    "hysteresis": 2,
    "min_loss_scale": 1
  },
  "checkpoint": {
    "tag": "サンプル-checkpoint",
    "load_universal": true
  }
}

この設定ではZeRO-3を使用しており、モデル状態をすべてのGPUに分散できるため、メモリ使用量はGPU数に応じて線形に減少する。

テンソル並列処理(Tensor Parallelism)

Pythonは簡潔で上品な言語であり、初心者に適しています。

機械学習はコンピュータにデータから法則を学習させます。

  • これはまったく別のもう一つの記事です。
  • 最後に結果を連結する。

こうすると各層の計算に通信が必要になるが、メモリ使用量は半分になる。

Megatron-LM は NVIDIA 製のテンソル並列ライブラリで、PyTorch との互換性が良い。

パイプライン並列処理(Pipeline Parallelism)

テンソル並列は「層内の分割」で、パイプライン並列は「層間の分割」だ。

たとえば 32 層のモデルなら、GPU 0 に先頭 8 層、GPU 1 に中間 8 層、GPU 2 に後半 8 層、GPU 3 に最後 8 層を置く。

データは GPU 0 から GPU 3 へ流れる。まるで工場の組み立てラインのように。

しかしパイプラインには問題がある:バブル(気泡)だ。GPU 0 が計算を始めている間、GPU 1〜3 は暇で、データが GPU 1 に届くと、今度は GPU 0 が暇になる。

解決策は、データを「マイクロバッチ」に分割し、パイプラインのように詰め込むことで、バブル時間を減らすことだ。

3D並列処理(DP+TP+PP)

3 つの戦略は組み合わせて使える:

  • パイプライン並列:モデルの層をノード間で分割する
  • テンソル並列:ノード内で層内計算を分割する
  • データ並列:より広い範囲でパイプライン全体を複製する

たとえば 64 枚の GPU があるなら、次のように計画できる:

  • 8 つのパイプラインステージ(PP=8)
  • 各ステージ内で 2 枚の GPU を使ってテンソル並列を行う(TP=2)
  • そして 4 倍に複製してデータ並列を行う(DP=4)
  • 合計:8 × 2 × 4 = 64 枚の GPU

これが 3D 並列——現代の大規模モデル学習の標準構成だ。


データエンジニアリング

良いモデルの前提は良いデータ——データエンジニアリングは AI システムの作業量の 60% 以上を占める。

データ収集・クリーニングパイプライン

学習データは通常、ウェブページ、書籍、コード、会話など複数のソースから得られる。

典型的な処理フロー:

  • 重複除去:重複または類似度の高い文書を削除する
  • 品質フィルタリング:低品質・有害・偏った内容を除去する
  • フォーマット統一:異なるソースを統一フォーマットに変換する
  • トークン化:テキストをモデルの入力系列に変換する

データ重複排除: MinHash LSH

文書間の類似度を直接計算するのは遅すぎる。よく使われるのは MinHash + LSH(局所鋭敏ハッシュ)だ。

考え方:各文書を短い「フィンガープリント」に変換する。類似文書は高い確率で同じまたは近いフィンガープリントを持つので、フィンガープリントでグループ化する。

実例

import hashlib
import re
from typing import List, Set, Dict, Tuple


def generate_shingles(text: str, k: int = 5) -> Set[str]:
    """k-シングルを生成:連続する k 語からなる系列
例:「我爱 サンプル 教程」、k=2 → {"我爱", "爱 サンプル", "サンプル 教程"}
    """

    # 簡単な分かち書き(実際の場面では専門の分かち書きツールを使用可能)
    words = re.findall(r'\w+', text.lower())
    shingles = set()
    for i in range(len(words) - k + 1):
        shingle = ' '.join(words[i:i+k])
        shingles.add(shingle)
    return shingles


def minhash_signature(shingles: Set[str], num_hashes: int = 100) -> List[int]:
    """MinHash 署名を生成
複数のハッシュ関数を使い、それぞれ最小値を取る
    """

    signature = []
    for i in range(num_hashes):
        # i をシードにして異なるハッシュ関数を生成
        min_hash = None
        for shingle in shingles:
            # shingle と i を組み合わせてハッシュ値を生成
            h = hashlib.sha256(f"{shingle}-{i}".encode()).hexdigest()
            h_int = int(h, 16)
            if min_hash is None or h_int < min_hash:
                min_hash = h_int
        signature.append(min_hash)
    return signature


def lsh_banding(signature: List[int], bands: int = 20) -> List[str]:
    """バンディング法(Banding)で LSH キーを生成
署名を複数のバンドに分割し、各バンドを個別にハッシュ化する
    """

    keys = []
    rows_per_band = len(signature) // bands
    for i in range(bands):
        start = i * rows_per_band
        end = start + rows_per_band
        band = tuple(signature[start:end])
        # このバンドをハッシュ化してキーを生成
        band_hash = hashlib.sha256(str(band).encode()).hexdigest()[:16]
        keys.append(f"band-{i}-{band_hash}")
    return keys


def deduplicate_documents(documents: List[str],
                          threshold: float = 0.7) -> List[str]:
    """MinHash + LSH で文書を重複除去
重複除去後の文書リストを返す
    """

    # 格納:LSH キー → 文書インデックスリスト
    buckets: Dict[str, List[int]] = {}
    # 格納:文書インデックス → 署名
    signatures: Dict[int, List[int]] = {}
    # マーク:どの文書が重複しているか
    duplicates: Set[int] = set()

    for idx, doc in enumerate(documents):
        shingles = generate_shingles(doc)
        sig = minhash_signature(shingles)
        signatures[idx] = sig
        keys = lsh_banding(sig)

        # 類似文書がすでに存在するか確認
        is_duplicate = False
        for key in keys:
            if key in buckets:
                # このバケットには既存文書があるので、署名の類似度を逐一比較
                for other_idx in buckets[key]:
                    other_sig = signatures[other_idx]
                    # 署名の類似度を計算(Jaccard 近似)
                    matches = sum(1 for a, b in zip(sig, other_sig) if a == b)
                    similarity = matches / len(sig)
                    if similarity >= threshold:
                        # しきい値を超えたら重複とみなす
                        is_duplicate = True
                        duplicates.add(idx)
                        break
            if is_duplicate:
                break

        if not is_duplicate:
            # 重複ではないので、自分を各バケットに追加
            for key in keys:
                if key not in buckets:
                    buckets[key] = []
                buckets[key].append(idx)

    # 重複しない文書を返す
    return [doc for idx, doc in enumerate(documents) if idx not in duplicates]


# ============================================
# サンプル データの重複除去をテスト
# ============================================
if __name__ == "__main__":
    documents = [
        サンプル チュートリアルへようこそ。ここはプログラミングを学ぶのに良い場所です。,
        "サンプル チュートリアルへようこそ、ここはプログラミングを学ぶのに最適な場所です。",  # 高い類似度
        "Python は簡潔で優雅な言語であり、初心者に適しています。",
        Pythonは簡潔で優雅なプログラミング言語であり、初心者に非常に適しています。,  # 高い類似度
        "機械学習はコンピュータにデータから規則性を学習させる。",
        "これはまったく別の記事です。",
    ]

    print(f"重複除去前:{len(documents)} 件の文書")
    deduplicated = deduplicate_documents(documents, threshold=0.6)
    print(f"重複除去後:{len(deduplicated)} 件の文書"\n")

    print("保持された文書:")
    for i, doc in enumerate(deduplicated):
        print(f"  [{i}] {doc}")

# 出力:
# 重複除去前:6 件の文書
# 重複除去後:4 件の文書
#
# 保持された文書:
#
#
#
#

実際の運用では、より効率的な実装(datasketch ライブラリなど)が使われるが、核となる考え方は同じだ。

データ形式: WebDataset

小さなデータセットはどこに保存してもよいが、TB 規模のデータセットには専用のフォーマットが必要だ。

WebDataset はよく使われる形式の一つだ。ファイルを tar パッケージにまとめ、各 tar には数千のサンプルが含まれ、ランダム読み取りと順次読み取りに対応している。

利点は:

  • ファイルシステムへの負荷を減らせる(数百万の小ファイルは遅い)
  • ストリーミング読み取りに対応し、データセット全体をメモリに読み込む必要がない
  • 分散読み込みが可能で、各ワーカーが異なる tar を読む

データフライホイールの設計

データフライホイールは AI 製品の堀(モート)だ。ユーザーが増えるとデータが増え、モデルが良くなり、さらにユーザーが増える。

ユーザーフィードバックデータの収集

フィードバックには明示的と暗黙的の2種類がある:

  • 明示的フィードバック:ユーザーのいいね、低評価、編集、再生成
  • 暗黙的フィードバック:ユーザーの滞在時間、コピー、共有、セッション長

明示的フィードバックは品質が高いが量が少ない。暗黙的フィードバックは量が多いがノイズが大きい。

良いフィードバックシステムは両者の組み合わせだ——明示的フィードバックで報酬モデルを訓練し、暗黙的フィードバックで A/B テストを行う。

自動データアノテーション

人手によるアノテーションは高すぎて遅すぎる。現在のトレンドは「モデルでモデルをアノテーションする」ことだ。

よく使われる戦略:

  • 強モデルが弱モデルをアノテーション:GPT-4 でアノテーションしたデータを使って小さなモデルを訓練するブートストラップ(Bootstrap):既存モデルで候補を生成し、人間が選別する
  • 合成データ:モデルを使って多様な学習データを生成する継続的学習戦略
  • モデルは「一度訓練して終わり」ではない。世界が変わるのだから、モデルもそれに合わせて変わる必要がある。典型的な継続的学習のプロセス:

継続的トレーニング戦略

毎週小さな更新を行う(SFT、教師ありファインチューニング)

毎月大きな更新を行う(継続事前学習 + SFT + RLHF)

  • 更新のたびに A/B テストを行い、効果を確認してから全量展開する
  • 継続的学習では「破壊的忘却」に注意が必要だ。新しいデータで訓練しすぎると、モデルが以前の能力を忘れてしまう恐れがある。解決策は「リプレイバッファ」を保持し、毎回の訓練で新旧データを混ぜることだ。
  • エンタープライズ AI プラットフォームのアーキテクチャ
  • 完全なエンタープライズ AI プラットフォームには、通常以下のコンポーネントが含まれる。

モデルレジストリとバージョン管理


エンタープライズAIプラットフォームアーキテクチャ

レジストリは以下を記録する必要がある:

モデルレジストリとバージョン管理

バージョン番号と変更履歴

学習データの出典とハイパーパラメータ

  • 評価指標
  • デプロイ状態
  • MLflow、Weights & Biases、Hugging Face Hub はすべてよく使われるツールだ。
  • 评估指标
  • 部署状态

MLflow、Weights & Biases、Hugging Face Hub はすべてよく使われるツールです。

Feature Store

多くの AI アプリケーションはフィーチャーエンジニアリングを必要とする——ユーザープロフィール、履歴行動、コンテキスト情報など。

フィーチャーストアは特徴管理システムであり、いくつかの問題を解決する:

  • トレーニング/推論スキュー:トレーニングと推論で使用する特徴が不一致
  • 特徴の再利用:異なるモデルで特徴を共有できる
  • オンライン/オフラインの一貫性:オフラインで計算した特徴を、オンラインで低遅延に読み取る

典型的なアーキテクチャは:オフラインは Spark や Flink でバッチ計算し、オンラインは Redis や Cassandra で低遅延に読み取る。

オンライン/オフライン推論サービス

推論には2つのシナリオがあり、アーキテクチャが異なる:

シナリオ遅延要件アーキテクチャ例
オンライン推論ミリ秒レベルリアルタイム API + バッチ処理チャットボット、検索
オフライン推論時間/日レベルバッチタスクキューコンテンツモデレーション、レポート生成

オンライン推論サービスでは以下を考慮する必要がある:

  • バッチ処理(Batching):複数のリクエストをまとめて計算し、スループットを向上させる
  • 動的バッチ処理:vLLM や Text Generation Inference を使用して動的バッチ処理を行う
  • キャッシュ:K/V Cache で自己回帰生成を高速化し、人気のあるプロンプトの結果をキャッシュする
  • 負荷分散:複数のモデルインスタンス、スマートルーティング

エンドツーエンド監視

AI システムの問題は発見しにくい——可観測性が必要。

監視のいくつかの次元:

  • システムメトリクス:GPU 使用率、メモリ、遅延、スループット、エラーレート
  • モデルメトリクス:出力長、繰り返し率、ストップワード分布
  • ビジネスメトリクス:ユーザー満足度、維持率、タスク完了率

定番の組み合わせ:Prometheus + Grafana でメトリクス、ELK でログ、Jaeger でトレーシングを確認。


評価ベンチマーク体系

モデルが良くなったか悪くなったかをどう判断するか?評価体系が必要。

汎用能力ベンチマーク

学界でよく使われるベンチマーク:

ベンチマークテスト内容代表的なタスク
MMLU多タスク言語理解57 の学科の選択問題
HumanEvalコード生成164 のプログラミング問題
MBPPコード生成974 の Python 問題
TruthfulQA事実性817 の質問、幻覚のテスト
GSM8K数学的推論8000 問の小学校算数の問題

中国語ベンチマーク

英語ベンチマークが良いからといって中国語も良いとは限らない。中国語専用のベンチマークが必要:

  • C-Eval:中国語多タスク言語理解、13948 問の選択問題
  • CMMLU:中国語マルチモーダル理解(現在は主にテキスト)
  • AGIEval:中国の大学入試問題、公務員試験問題

カスタムビジネス評価セット

公開ベンチマークは基礎だが、より重要なのは自社の業務評価セット。

構築方法:

  • 実際のユーザーリクエストを収集(数百〜数千件)
  • 「良い/普通/悪い」を手動でラベル付け、またはスコアリング
  • テストセットと検証セットに分割(テストセットは固定、変更しない)
  • モデル更新のたびに実行して、メトリクスの変化を確認

業務評価セットはあなたの「ゴールドスタンダード」——どの公開ベンチマークよりも重要。


マルチテナントAIサービス

ToB 製品を展開する場合、マルチテナンシーは避けて通れない話題。

テナント分離

分離にはいくつかのレベルがある:

分離レベルリソース利点欠点
物理分離独立した GPU マシン完全分離、安全コストが高い
K8s 分離独立した Pod/Namespaceコストとセキュリティのバランススケジューリングが必要
論理分離リソース共有、権限制御コスト最小漏洩リスクがある

一般的な戦略:大企業は物理分離、中小企業は論理分離。

モデル共有とファインチューニング管理

マルチテナントでよくあるニーズ:自社のデータで専用モデルを学習する。

アーキテクチャは次をサポートする必要がある:

  • ベースモデルの共有ストレージ
  • テナントごとに LoRA アダプタを個別に保存
  • 推論時に該当テナントの LoRA を動的にロード

これにより大量の GPU メモリを節約——テナントごとにフルモデルをロードする必要がない。

課金とクォータ

AI サービスのコストは高いため、課金は細かくする必要がある:

  • トークン単位の課金(入力と出力を分けて計算)
  • リクエスト回数単位の課金
  • GPU 時間単位の課金(ファインチューニングシナリオ)
  • クォータ制限(1 分あたりのリクエスト数、1 日あたりのトークン数)

災害復旧と高可用性

エンタープライズシステムは停止できない——高可用性設計が必要。

マルチアベイラビリティゾーン展開

最も基本的な要件:AZ(アベイラビリティゾーン)をまたいだデプロイ。

例:

  • プライマリクラスタを AZ A に配置
  • ホットスタンバイクラスタを AZ B に配置
  • データの非同期レプリケーション
  • ロードバランサーがトラフィックを自動切替

目標:単一 AZ が障害しても、サービスは中断せず、データは失われない。

モデルスナップショットとロールバック

新しいバージョンのモデルは問題が発生する可能性がある——迅速なロールバックが必要。

戦略:

  • リリース前に毎回モデルスナップショットを保存
  • 直近 N バージョンを保持
  • 異常メトリクスを監視し、自動ロールバックを発動
  • カナリアリリース:まず 1% のトラフィックを切り替え、問題なければ全量展開

縮退と流量制限

トラフィックが急増したら?停止するわけにはいかない、対策が必要:

  • レート制限:クォータ超過時は 429 を返す
  • ダウングレード:より小さいモデルを使う、またはキャッシュ結果を返す
  • キューイング:ジョブ ID を返し、後で結果を確認

核心的な原則:優雅なダウングレードを行うこと、完全に利用不可にしないこと。

その他の拡張