[{"content":"背景 本番でGPU上でサービングしていたペルソナ逸脱判定モデル（AIキャラクターがペルソナを外れて一般アシスタントのように答えたり、リクエストを拒否したりする状況を捉える二値分類器）を国産NPUに移す実験を行いました。結論から言うと、「公式非対応」という壁の前で原因を一つずつ突き止めた末に、移植に成功しました。オンデバイス推論は9.7ms（固定512トークン、ジッターほぼゼロ） ほどで、既存のクラウドGPUサービングと比べても遜色ないレイテンシと安定性を確認できました。この記事は、その過程で何を試し、どこで詰まり、どう解決したのかを順に記録したものです。\nまず、国産NPUにアクセスできた背景から。韓国は政府主導で国産AI半導体を複数の経路で供給しています。マーケティング文句ではなく、実際にスタートアップや研究者がRebellions ATOM系を無償／実証で確保できる窓口が複数存在します。\nK-Cloudプロジェクト（科学技術情報通信部・NIPA、2023年から3年）— 「AI半導体ファーム構築・実証」事業。クラウド3社（Naver Cloud・KT Cloud・NHN Cloud）とAI半導体3社（Rebellions・Sapeon・FuriosaAI）がコンソーシアムを組み、国産NPUベースの高性能クラウドを構築・検証します。（ZDNet、NIPA公告） 高性能コンピューティング支援事業（科学技術情報通信部・NIPA、2026年に約167億ウォン規模）— 選定された需要企業に国産NPUサーバを無償提供します。供給企業もKT Cloud、Gabiaなど複数が選ばれ、例えばGabiaはRebellions ATOM-Maxを無償提供しています。（Etoday、VentureSquare） つまりCSP（KT/NHN/Naver/Gabia…）×事業（ファーム実証／無償提供）の組み合わせで、ATOM・ATOM+・ATOM-Maxが多様な窓口から供給されています。私たちもこのうち一つの経路で、KT CloudのAI Nexus（内部的にはBackend.AIベース）という形でATOM+ 4枚を確保しました。\nこの移植で期待した利得は三つでした。① 常時GPUサービングのコスト削減（政府提供ハードウェア）、② より低く安定した推論レイテンシ、③ NVIDIA GPU以外のベンダー選択肢の確保です。\n問題はその次でした。\nスタックを一望する — ソフトウェアからハードウェアまで 本題に入る前に、この作業がどの階層の上で起きているのかを整理します。テキスト一行が判定結果になるまでには、ソフトウェア（上）からハードウェア（下）へと連なる複数の階層を通過します。（各用語の定義は末尾の用語整理にまとめてあります。）\n階層 役割 GPU（一般的な経路） NPU（ATOM+、私たちの場合） アプリケーション サービスロジック（プーリング + 分類ヘッド） ペルソナ逸脱判定分類器 ← 同じ モデル（バックボーン） テキストをベクトルにエンコードするニューラルネット ModernBERT(mmBERT) ← 同じ 統合レイヤ モデル定義を実行層に接続 HuggingFace transformers ライブラリ（直接実行） optimum-rbln コンパイラ モデルを実行形態に変換 torch.compile / TensorRT（任意 — なくても即時実行） rebel-compilerによる事前コンパイル必須 ランタイム 実際の演算実行 PyTorch + CUDA rebel.Runtime ハードウェア 物理アクセラレータ NVIDIA GPU Rebellions ATOM+（RBLN-CA22） 肝はコンパイラ層です。GPUではこのスタックがおおむね透過的でモデルをそのまま実行できますが、NPUではコンパイラが関門になります。この記事の大半は、結局この一つの層を通す話です。\nちょっと待った — 「コンパイル」とは何のことか NPUの話を続けるには、「モデルをコンパイルする」という言葉の意味を押さえておく必要があります。手短に言えば、PyTorchモデルを特定のアクセラレータがそのまま実行できる形態にあらかじめ翻訳しておくことです。GPUの基本であるeager実行（Pythonの一行ごとにカーネルをその場で呼び出す）は柔軟ですがレイテンシがばらつき、コンパイルはグラフ全体を一つの決定的なバイナリに固めて低レイテンシ・低ジッターを得ます。NPUでは事実上このコンパイルが必須であり、後述のreference_compileや動的shape分岐をオフにしなければならなかったのも、すべてこの「グラフをあらかじめ固める」という制約から来ています。\n概念そのもの — eager vs コンパイル、グラフキャプチャ・演算融合・カーネルlowering（OpenAI Tritonなど）・静的shape — を図とインタラクティブなデモで一から解き明かした別記事があります → ニューラルネットのコンパイルを、やさしく理解する。ここでは私たちの事例に必要な分だけ押さえて進みます。\n問題認識 — NPUは事前にコンパイルしなければならない GPUはPyTorchモデルをそのまま実行できますが、NPUは違います。「このモデル、この入力サイズ」で事前にコンパイルし、NPU専用バイナリに変換する必要があります。この変換を担うのがRebellionsのコンパイラrebel-compilerで、その上に標準モデル定義をつなぐ統合レイヤoptimum-rblnが載ります。\nところが私たちのバックボーンはmmBERT、つまり**ModernBERTアーキテクチャ**でした。確認してみると、対応リストから外れていました。\noptimum-rblnが公式対応するエンコーダはbert / distilbert / roberta / xlm-robertaのみ。ModernBERTは非対応。 Rebellions Model ZooにもModernBERT系はありません。 普通はこの時点でXLM-Rのような対応モデルへ乗り換える選択をすることになります。しかし私たちのモデルの精度はすでにmmBERTで検証済みで、バックボーンを替えると再学習・再検証のコストが大きくなります。そこで自分で対応を実装してみることにしました。\n仮説 — 「本当に対応が不可能なのか?」 「非対応」というラベルの実体を覗いてみると、実際に詰まる原因は二種類です。(1) コンパイラが理解できない演算（op）があるか、(2) 統合レイヤにそのモデル用のラッパーがないか。\nModernBERTが新たに導入したとされる要素を一つずつ分析しました。\nModernBERTの要素 すでに対応済みの場所 交互のlocal/global attention Gemma2(1:1)、Gemma3(5:1) dual RoPE（global θ + local θ） Gemma3 大きなvocab（256k） Gemma2/3（同一vocab） GLU/GeGLU MLP LLaMA、Qwen2/3、Gemma全般 SDPA attention ほぼ全ての対応デコーダ すべてすでに対応済みのデコーダモデル（Gemma2/3など）に入っている要素でした。つまりコンパイラのop互換性は事実上検証済みで、実際の違いは**「エンコーダかデコーダか」だけ、という結論に至りました。そしてエンコーダ用のベースクラス（RBLNModelForMaskedLM）はすでに存在していました。結局(2)、つまりラッパーがないだけ**でした。\n注意すべき点はattentionの実装でした。ModernBERTはデフォルトがFlash Attention 2（HuggingFaceのflash_attention_2バックエンド）ですが、これはNVIDIA CUDA専用カーネルなので、rebel-compilerがNPU向けにコンパイルできません。幸い、PyTorch標準の抽象APISDPA（scaled_dot_product_attention） に強制すると、コンパイラがこれを認識して自前のNPUカーネルに翻訳してくれます。精度を落とさずバックエンドだけ差し替えるわけです。\n図で見ると、私たちが移そうとしていたmmBERT-base（ModernBERTアーキテクチャ）の計算グラフで詰まった箇所はちょうど二つだけです。残りの演算（RoPE、GeGLUなど）はすでにコンパイラが対応しています（緑）。\nflowchart TB IDS[\u0026#34;input_ids · [1,512]\u0026#34;] --\u0026gt; EMB[\u0026#34;Token Embedding + LayerNorm\u0026#34;] MASK[\u0026#34;attention_mask · [1,512]\u0026#34;] -.-\u0026gt; ATTN EMB --\u0026gt; LN1 subgraph LYR[\u0026#34;エンコーダ層 × 22 (mmBERT-base = ModernBERT)\u0026#34;] direction TB LN1[\u0026#34;LayerNorm (pre-norm)\u0026#34;] --\u0026gt; ROPE[\u0026#34;RoPE (global θ / local θ)\u0026#34;] ROPE --\u0026gt; ATTN[\u0026#34;Attention\u0026lt;br/\u0026gt;local (sliding window) · global (3層ごと)\u0026#34;] ATTN --\u0026gt; RES1[\u0026#34;＋ residual\u0026#34;] RES1 --\u0026gt; LN2[\u0026#34;LayerNorm\u0026#34;] --\u0026gt; MLP[\u0026#34;GeGLU MLP\u0026#34;] --\u0026gt; RES2[\u0026#34;＋ residual\u0026#34;] end RES2 --\u0026gt; FLN[\u0026#34;final LayerNorm\u0026#34;] --\u0026gt; POOL[\u0026#34;mean pooling\u0026#34;] --\u0026gt; HEAD[\u0026#34;linear head → logits(2)\u0026#34;] ATTN -.- NOTE1[\u0026#34;⚠ 問題 ① Flash Attention 2 (CUDA専用)\u0026lt;br/\u0026gt;→ SDPA に置換 · sliding_window_mask·unpadding(動的 shape) 無効化\u0026#34;] LYR -.- NOTE2[\u0026#34;⚠ 問題 ② reference_compile=False\u0026lt;br/\u0026gt;(forward 内部の torch.compile 除去)\u0026#34;] class ATTN problem class ROPE,MLP okc class NOTE1,NOTE2 note classDef problem fill:#7f1d1d,stroke:#f87171,stroke-width:3px,color:#fff classDef okc fill:#14532d,stroke:#4ade80,color:#fff classDef note fill:#3a2f0b,stroke:#e0a020,color:#fbe8a6つまりアーキテクチャ全体が問題なのではなく、attentionカーネル（FA2） と forward内のtorch.compile（reference_compile） の二箇所だけがNPUコンパイルと衝突しました。この二つをアダプタから取り除くのが次のステップでした。\nアダプタ作成 — 制御すべき三点 兄弟クラスRBLNRobertaForMaskedLMのパターンを参考にModernBERTアダプタを作成しました。制御すべき点は三つです。\nconfig段で非互換オプションをオフにする — attn_implementation=\u0026quot;sdpa\u0026quot;、reference_compile=False（ModernBERTはforward内でtorch.compileを呼びますが、これがtrace/exportを壊します）。 wrapper段で非互換kwargsを遮断する — unpadding/sequence-packing関連の引数（indices、cu_seqlens、sliding_window_maskなど）を明示的に無効化し、動的shapeの分岐を除去。 入力シグネチャを単純化する — input_ids + attention_maskの二つだけ。 ここまではNPUなしでも検証できます。MacBookでtorch.jit.traceとtorch.exportによりグラフを取り出してみると、異なる二種類の入力でeager比の誤差max|diff| = 0.0。数値的に完全に同一のグラフが出ました。ここまで確認できれば、移植可能性への確信が持てます。残るは実機での検証だけです。\n実機コンパイル — 最初の試みは失敗 ATOM+セッションに接続し（コンテナにrebel-compiler・optimum-rblnがプリインストール済みなので別途インストールは不要でした）、コンパイルを実行しました。最初の試みは次のエラーで失敗しました。\nValueError: Configuration for RBLNModernBertForMaskedLMConfig not found. 原因を追ってみると、参照したoptimum-rblnは最新main基準でしたが、実際にコンテナに入っていたのは0.10.2リリースで、このバージョンはconfigクラスを名前ベースのレジストリ（optimum.rblnのトップレベルattr、または内部のCONFIG_MAPPING）からしか探しませんでした。カスタムアダプタのconfigはそこに登録されていないので「見つからない」で失敗したわけです。SDKのバージョンドリフトが作った罠でした。\n解決は一行でした。モデルクラスにconfigクラスを直接指定し、レジストリ探索をスキップさせました。\nclass RBLNModernBertForMaskedLM(RBLNModelForMaskedLM): _rbln_config_class = RBLNModernBertForMaskedLMConfig # レジストリを回避 再実行すると今度は通りました。\n[rebel-compiler] Target NPU: RBLN-CA22 [rebel-compiler] Tensor parallel size: 1 Computation graph generation ████████████ 100% Computation graph optimization ████████████ 100% saved -\u0026gt; compiled_model.rbln rbln-smiで確認した実機はRBLN-CA22（ATOM+）、15.7GiB。「非対応」モデルが国産NPU上でコンパイルされた瞬間でした。コンパイル自体は十数秒で終わりました。\n実戦適用 — 分類器を丸ごと移植する ここまではバックボーン（MaskedLM）だけをコンパイルしたものでした。実際にサービスするのは、その上にmean pooling + linear headを載せたペルソナ逸脱判定分類器です。これは標準のHuggingFaceアーキテクチャではなくカスタムnn.Moduleなので、optimum-rblnの高レベル経路ではなくコンパイラの低レベルAPIを直接使うほうがすっきりします。\nflowchart TD T[\u0026#34;原文テキスト\u0026#34;] --\u0026gt; TOK[\u0026#34;Tokenizer\u0026lt;br/\u0026gt;input_ids · attention_mask · [1, 512]\u0026#34;] TOK --\u0026gt; BB subgraph G[\u0026#34;コンパイル単位 — rebel.compile_from_torch ⇒ compiled_model.rbln （ATOM+ NPUで実行）\u0026#34;] direction TB BB[\u0026#34;mmBERTバックボーン · SDPA\u0026#34;] --\u0026gt; MP[\u0026#34;mean pooling\u0026#34;] MP --\u0026gt; LH[\u0026#34;linear head\u0026#34;] end LH --\u0026gt; LOG[\u0026#34;logits (2)\u0026#34;] LOG --\u0026gt; P[\u0026#34;softmax → 逸脱確率\u0026#34;]TokenizerはCPUで走り、その下のバックボーン+プーリング+ヘッドが一つのコンパイル単位にまとまってNPUで実行されます。rebel.compile_from_torch(model, input_info=[(\u0026quot;input_ids\u0026quot;,[1,512],\u0026quot;int64\u0026quot;), (\u0026quot;attention_mask\u0026quot;,[1,512],\u0026quot;int64\u0026quot;)]) の一度の呼び出しで、この三つを単一グラフにコンパイルしました。\ncompiled_model.rblnは直列化されたバイナリなのでそのまま開いて見ることはできませんが、コンパイラが内部的に扱うグラフを展開するとおおよそこのような形です（概念的表現）。\n# compiled_model.rbln — 内部グラフ（概念的表現） target: RBLN-CA22 (ATOM+) tensor_parallel: 1 dtype: fp16 in %input_ids : i64[1, 512] in %attention_mask : i64[1, 512] %emb = rbln.embedding %input_ids -\u0026gt; f16[1,512,768] %h0 = rbln.layernorm %emb # × N encoder layers (attention → mlp): %a = rbln.sdpa_attention %h{i}, mask=%attention_mask @kernel=attn_fused_ca22 %m = rbln.geglu_mlp %a @kernel=geglu_fused_ca22 ... %enc = rbln.layernorm %hN -\u0026gt; f16[1,512,768] %pool = rbln.mean_pool %enc, mask=%attention_mask -\u0026gt; f16[1,768] %log = rbln.linear %pool, W=const[768,2] -\u0026gt; f16[1,2] out %log 先に説明したコンパイル段階がそのまま現れています — 演算が@kernel=..._ca22という形のATOM+専用カーネルへloweringされ、すべてのテンソルshapeが[1,512,...]に静的に固定されています。そして最も重要な検証 — CPUとNPUの予測は一致するか?\n入力タイプ CPU 逸脱確率 NPU 逸脱確率 逸脱発話 1 1.0000 1.0000 通常発話 1 0.0007 0.0007 逸脱発話 2（韓国語） 1.0000 1.0000 通常発話 2（韓国語） 0.0002 0.0002 logitsレベルの数値誤差は最大〜4e-2（NPUのFP16演算特性）でしたが、確率・判定は小数第4位まで一致しました。予測結果はそのまま再現されました。\nベンチマーク 同一モデル、batch=1、固定512トークン基準。\n指標 ATOM+（オンデバイス） 平均レイテンシ 9.67 ms p50 / p99 9.66 / 9.73 ms max 10.6 ms 単一ストリームのスループット 103.5 inf/s 数字と同じくらい印象的だったのが分布の安定性です。p50 9.66ms / p99 9.73ms / max 10.6msで、実質ジッターがありません。コンパイル済みの固定グラフを決定的に実行するので、レイテンシが跳ねず一定に保たれます。これまで使っていたクラウドGPUサービングと比べても十分に競争力のある水準でした。消費電力もカード当たり85W級です。\n期待は当たったか まとめると、三つのうち低レイテンシ・安定性は期待以上に確認できました（9.67ms・ジッターほぼゼロ）。ベンダー選択肢も「非対応モデルすら移植できる」ことを実証して開けました。コスト削減に向けたサービング構成は現在準備中です。\n教訓 「非対応」はたいてい「ラッパーがない」だ。 コンパイラが理解する演算（op）の組み合わせで出来たモデルなら、薄いアダプタで移植できます。 SDPA標準化の力。 ベンダー依存カーネル（FA2）ではなくPyTorch標準の抽象APIに落としておけば、バックエンド（GPUでもNPUでも）が自前のカーネルに翻訳してくれます。移植性の核心です。 ローカルで可能な限り検証してから実機へ。 trace/exportでグラフ等価性（誤差0）をローカルで確認しておくと、実機でデバッグする表面積が大きく減ります。 SDKのバージョンドリフトに注意。 ドキュメント／最新mainと実際にデプロイされたリリースの内部APIが異なることがあります。今回はconfigレジストリ方式の違い一行が最初のコンパイルを塞ぎました。 「受け取ったが使えない」状態のまま国産NPUを放置しているチームは、少なくないと思われます。試行錯誤はありましたが、少なくともエンコーダ系のモデルなら確かに移植可能です。政府が多様な経路でハードウェアを供給しておいた今、一度本気で挑んでみる価値はあります。\n用語整理 NPU (Neural Processing Unit) — ニューラルネット演算に特化した半導体。汎用のGPUと違い、実行するモデルと入力サイズをあらかじめ決めてコンパイルした上で、専用バイナリで動かす。 エンコーダモデル / ModernBERT / mmBERT — テキストをベクトル表現に変換する（生成ではなく理解中心の）ニューラルネット。BERT系の最新アーキテクチャがModernBERTで、その多言語版がmmBERT。 コンパイル（NPU文脈） — PyTorchモデルを特定の入力形態に合わせてNPU専用の実行バイナリ（.rbln）に変換する過程。GPUの即時実行と対比される。 rebel-compiler / optimum-rbln / rebel.Runtime — それぞれRebellionsのコンパイラ、HuggingFaceモデルをコンパイラに接続する統合レイヤ、コンパイル済みバイナリをNPUで実行するランタイム。 SDPA vs Flash Attention 2 — どちらもattention演算の実装。ここで言うFA2はHuggingFaceのflash_attention_2バックエンド（NVIDIA CUDA専用のflash-attnカーネル）で、rebel-compilerがコンパイルできない。SDPA（scaled_dot_product_attention）はPyTorch標準の抽象APIで、各バックエンドが自前のカーネルに翻訳できるため移植性が高い。（参考：RBLNにもデコーダのKVキャッシュ用の独自flash_attnオプションがあるが、これはHFのFA2バックエンドとは別物で、エンコーダには該当しない。） RoPE / GLU・GeGLU — それぞれ位置情報を回転で注入する埋め込み手法、ゲーティングを適用したMLP構造。最新のトランスフォーマーで広く使われており、Rebellionsのコンパイラもすでに対応している。 MaskedLM — 文の一部を隠して復元するように学習されたエンコーダの基本形態（バックボーン）。ここにプーリング・分類ヘッドを載せて分類器にする。 mean pooling — トークンごとのベクトルを平均して、文一つのベクトルに要約する方式。 ATOM+ / RBLN-CA22 — RebellionsのNPUカード（ATOM+）と、その内部ターゲット識別子（RBLN-CA22）。 参考 K-Cloudプロジェクト / AI半導体ファーム構築・実証 — ZDNet、NIPA統合公告 2026 高性能コンピューティング支援事業（国産NPU無償提供）— Etoday、VentureSquare Rebellions optimum-rbln（アダプタが参照した統合レイヤ）— GitHub Rebellions Model Zoo — GitHub Rebellions RBLN SDKドキュメント — docs.rbln.ai ","permalink":"http://blog.caveduck.io/ja/posts/modernbert-on-rebellions-npu/","summary":"本番でGPUサービングしていたペルソナ逸脱判定モデル（mmBERTベースの分類器）を国産NPU（Rebellions ATOM+）へ移すまで — 問題認識からアダプタ作成、実機コンパイル、ベンチマークまでの道のり。","title":"政府が無償で配る国産NPU、本当に使えるのか? — 非対応モデル移植の実験記"},{"content":" この記事は国産NPUに未対応モデルを移植した話から派生したコンセプト解説です。あちらの記事が「実際にどう突破したか」なら、こちらの記事はその土台にある**「モデルをコンパイルするとは一体どういう意味なのか」**を最初から解きほぐします。ディープラーニングを少し知っている方なら無理なく読めるように書きました。\n「コンパイル」というおなじみの言葉から プログラミングにおけるコンパイルとは、人が読むソースコード(C、Rustなど)を、CPUがそのまま実行する機械語へとあらかじめ翻訳する工程です。翻訳を先に済ませておく代わりに、実行時は速くなります。\nニューラルネットのコンパイルもまったく同じ発想です。PyTorchで書いたモデルを、特定のアクセラレータ(GPU・NPUなど)がそのまま実行できる形へとあらかじめ翻訳しておきます。たった一つ違うのは、翻訳の出発点です。\n一般的なコンパイラの入力 = テキストのソースコード ニューラルネットコンパイラの入力 = 演算グラフ(computation graph) 演算グラフとは、「このテンソルに行列積 → 次にアテンション → 次に正規化 → …」のように、テンソル演算がつながった有向グラフです。モデルのforward()が行うことを図として展開したもの、と考えればよいでしょう。\nただし「一直線に流れるパイプライン」は教科書に出てくる単純なモデルの話であって、実際のモデルは複数のモジュールが分岐し合流するグラフです。たとえば画像編集モデル(Qwen-Image-Edit系)は、入力画像・編集プロンプト・タイムステップがそれぞれ別々のエンコーダを通り、一つのバックボーンで合流し、デノイジングループを回りながらフィードバックされます。\nflowchart TD IMG[\u0026#34;入力画像\u0026#34;] --\u0026gt; VAE[\u0026#34;VAEエンコーダ\u0026#34;] IMG --\u0026gt; VIT[\u0026#34;ビジョンエンコーダ\u0026lt;br/\u0026gt;Qwen2.5-VL\u0026#34;] TXT[\u0026#34;編集プロンプト\u0026#34;] --\u0026gt; TENC[\u0026#34;テキストエンコーダ\u0026lt;br/\u0026gt;LLM\u0026#34;] TS[\u0026#34;timestep t\u0026#34;] --\u0026gt; TEMB[\u0026#34;時間埋め込み\u0026#34;] VAE --\u0026gt; LAT[\u0026#34;画像潜在トークン\u0026#34;] VIT --\u0026gt; COND[\u0026#34;意味条件トークン\u0026#34;] TENC --\u0026gt; TTOK[\u0026#34;テキストトークン\u0026#34;] LAT --\u0026gt; BB COND --\u0026gt; BB TTOK --\u0026gt; BB TEMB --\u0026gt; BB subgraph BB[\u0026#34;MMDiTバックボーン · マルチモーダルジョイントアテンション × Nブロック\u0026#34;] direction TB JA[\u0026#34;ジョイントセルフアテンション\u0026lt;br/\u0026gt;画像 ↔ テキスト\u0026#34;] --\u0026gt; FF[\u0026#34;MLP\u0026#34;] FF -. 残差 .-\u0026gt; JA end BB --\u0026gt; EPS[\u0026#34;予測ノイズ ε\u0026#34;] EPS --\u0026gt; LOOP{\u0026#34;デノイジングループ\u0026#34;} LOOP --\u0026gt;|\u0026#34;次のステップ t−1\u0026#34;| BB LOOP --\u0026gt;|\u0026#34;完了\u0026#34;| VDEC[\u0026#34;VAEデコーダ\u0026#34;] VDEC --\u0026gt; OUT[\u0026#34;編集された画像\u0026#34;]同じ入力(画像)が二手に分岐して別々のエンコーダに入り、テキスト・タイムステップとともにバックボーンで**合流(merge)**し、ブロックの中には残差接続が、外側にはデノイジングのフィードバックループがあります。コンパイラが実際に向き合うグラフは、このように分岐・合流・反復が絡み合った形です。\nここで一つ重要な事実があります。一般的なコンパイラの前段である字句解析(lexical analysis、ソーステキストをトークンに分割する)とパース(parsing、トークンを構文木にする)は、ニューラルネットのコンパイルには存在しません。入力がすでに構造化されたグラフ(一般的なコンパイラでいえばAST/IRに相当)として与えられるからです。そのためニューラルネットのコンパイルは、コンパイラパイプラインの前段をまるごとスキップし、事実上IR最適化とコード生成の段階から始まります。\nでは、なぜコンパイルするのか? — eager実行の代償 PyTorchのデフォルトの実行方式はeager実行です。Pythonのコードが1行実行されるたびに、それに対応する演算カーネルをその場で即座に呼び出します。会話するように一文ずつ通訳してくれる通訳者に近いイメージです — 柔軟で、途中で値を出力しながらデバッグするのも簡単です。\n問題はコストです。演算の一つ一つごとにPython解釈 → カーネルディスパッチ → 実行が繰り返され、このオーバーヘッドが毎回少しずつ違って乗ってきます(=遅延がばらつく、ジッタ)。汎用GPUはカーネルが非常に速く柔軟なので、この方式でも十分よく動きますが、NPUのような特化型アクセラレータは何をどの順序で実行するかを事前に把握してこそ本来の性能が出ます。\nコンパイルとは、この通訳者を翻訳書に変える作業です。実行前にグラフ全体を受け取り、一つの決定的な実行単位として固めておけば、リクエストごとにその場で決めることがなくなり、実行がばらつきません。\n言葉よりも実際に見るほうが早いです。以下で同じモデルをeagerとコンパイルの二つの方式で何度も回し、遅延分布がどう違うかを比べてみてください。\neagerはopを一つずつディスパッチするため平均も高く、分布が広く散らばります。コンパイルは単一のグラフを決定的に実行するので、ほぼ一点に集まります。低遅延・低ジッタが求められる実サービスにおいてコンパイルが持つ価値が、ここに現れます。\nコンパイラがその間に行うこと modelを一つ渡すと、コンパイラはおおよそ5つの段階を経ます。\nflowchart TD A[\u0026#34;① グラフキャプチャ\u0026lt;br/\u0026gt;trace / export\u0026#34;] --\u0026gt; B[\u0026#34;② 演算融合・最適化\u0026#34;] B --\u0026gt; C[\u0026#34;③ ハードウェアカーネルへの落とし込み\u0026lt;br/\u0026gt;lowering\u0026#34;] C --\u0026gt; D[\u0026#34;④ 静的shapeの固定\u0026#34;] D --\u0026gt; E[\u0026#34;⑤ メモリプランニング\u0026#34;] E --\u0026gt; F([\u0026#34;実行バイナリ\u0026#34;])① グラフキャプチャ — Pythonをグラフへ まず、モデルが実際にどんな演算を行うのかをグラフとして抽出します。2つの方式があります。\ntrace (torch.jit.trace): サンプル入力を一度実際に流し込み、通った演算を記録する。シンプルですが、ifで分かれる分岐のようなものは「そのとき通った経路」だけが捕捉されます。 export (torch.export): コードを静的に解析し、制御フローまで含めたグラフを抽出する。より頑健です。 ② 演算融合 — 複数の演算を一つに 隣接する演算を一つにまとめます。たとえば行列積 → バイアス加算 → 活性化関数を別々に実行すると、中間結果をメモリに3回書き込んでは読み込みます。これを一つの融合カーネルにまとめれば、メモリの往復が減って格段に速くなります。\nflowchart LR subgraph before[\u0026#34;融合前 — カーネル3回、メモリ往復 多\u0026#34;] direction LR M1[\u0026#34;matmul\u0026#34;] --\u0026gt; B1[\u0026#34;+bias\u0026#34;] --\u0026gt; A1[\u0026#34;gelu\u0026#34;] end subgraph after[\u0026#34;融合後 — カーネル1回\u0026#34;] direction LR F1[\u0026#34;fused_matmul_bias_gelu\u0026#34;] end before -.最適化.-\u0026gt; after③ ハードウェアカーネルへの落とし込み(lowering) 標準的な演算(行列積、アテンション…)の一つ一つを、そのチップ専用の命令・カーネルへと翻訳します。GPU側の例を見ると感覚がつかめます。PyTorch torch.compileのデフォルトバックエンド(TorchInductor)は、演算をOpenAI Tritonへと落とし込みます。TritonはGPUカーネルをPython風の文法で記述する言語で、コンパイラが融合された演算に合わせたカーネルをTritonコードとしてその場で生成し、それをさらにGPUバイナリ(PTX/cubin)へと落とし込みます。ベンダーがあらかじめ用意したライブラリカーネル(cuBLASなど)を呼ぶ代わりに、グラフに合わせてカーネルを生成するわけです。NPUコンパイラも発想は同じです — コード生成バックエンドがTritonではなくそのチップ専用である、という点だけが異なります。\n④ 静的shapeの固定 入力サイズを事前に確定させます(例: [1, 512])。サイズが固定されると実行時の分岐が消えて決定的な経路ができあがり、融合やメモリプランニングもより積極的に行えます。NPU関連の記事で「固定512トークン」が繰り返し登場するのはこのためです。代償は柔軟性です — コンパイル時点で決めたサイズを外れる入力は(再コンパイルするかパディングしなければ)入れられません。\n⑤ メモリプランニング 各演算が使うバッファの配置と再利用を実行前に確定させます。実行中にメモリをその場で確保・解放しないので、その分だけ速く、一定になります。\nコンパイルの成果物はどんな姿か 成果物は、ハードウェアがそのまま食べられるバイナリです(Rebellionsなら.rbln)。シリアライズされた形なのでそのまま開いて見ることはできませんが、内部のグラフを概念的に展開するとこんな姿になります。\n# compiled_model — 内部グラフ(概念的な表現) target: RBLN-CA22 dtype: fp16 in %input_ids : i64[1, 512] in %attention_mask : i64[1, 512] %emb = embedding %input_ids -\u0026gt; f16[1,512,768] %h0 = layernorm %emb # × N encoderレイヤー: %a = sdpa_attention %h{i}, mask=%attention_mask @kernel=attn_fused %m = geglu_mlp %a @kernel=geglu_fused ... %pool = mean_pool %enc, mask=%attention_mask -\u0026gt; f16[1,768] %log = linear %pool, W=const[768,2] -\u0026gt; f16[1,2] out %log 先ほど見た段階がそのまま表れています — 演算が@kernel=..._fusedという形で専用カーネルにマッピングされ、すべてのテンソルのshapeが[1,512,...]と静的に固定されています。\nすでに存在するもの — ONNXとコンパイルのエコシステム ここまで「コンパイラ」を一つにひとまとめにしてきましたが、実際には複数の標準やツールが層をなしています。そのハブに頻繁に登場するのが**ONNX(Open Neural Network Exchange)**です。\nONNXはコンパイラではなく、演算グラフを収める公開標準フォーマットです。PyTorchであれTensorFlowであれ、どこで作ったモデルでもONNXへエクスポート(export)すれば、そのグラフを読める複数のランタイム・コンパイラがそれぞれのバックエンドで実行します。モデルを作ったフレームワークとデプロイ先を**分離(decouple)**する共通語というわけです。\nflowchart TD PT[\u0026#34;PyTorch\u0026#34;] --\u0026gt; ONNX TF[\u0026#34;TensorFlow\u0026#34;] --\u0026gt; ONNX JAX[\u0026#34;JAX\u0026#34;] --\u0026gt; ONNX ONNX[\u0026#34;ONNX · 共通グラフ標準\u0026#34;] --\u0026gt; ORT[\u0026#34;ONNX Runtime\u0026#34;] ONNX --\u0026gt; TRT[\u0026#34;TensorRT · NVIDIA\u0026#34;] ONNX --\u0026gt; OV[\u0026#34;OpenVINO · Intel\u0026#34;] ONNX --\u0026gt; NPU[\u0026#34;ベンダーNPUコンパイラ\u0026#34;]よく出会うツールを性格別に整理すると、こうなります。\nツール 性格 役割 ONNX 標準フォーマット フレームワーク間のグラフ交換(それ自体はコンパイラではない) ONNX Runtime ランタイム + 最適化 グラフを最適化したうえで複数の実行バックエンド(Execution Provider)で実行 TensorRT コンパイラ + ランタイム NVIDIA GPU専用の高性能推論 Apache TVM コンパイラスタック CPU/GPU/アクセラレータなど多様なバックエンドを狙うオープンなコンパイラ XLA コンパイラ JAX・TensorFlow(およびtorch/XLA)、TPU中心 torch.compile コンパイラ PyTorch内蔵 — TorchInductor → Triton OpenVINO コンパイラ + ランタイム Intel CPU/iGPU/NPU対象 optimum-rbln / rebel-compiler ベンダースタック Rebellions NPU対象(このシリーズの事例) 大きな流れはたいてい① フレームワークからグラフを抽出し(キャプチャ/export) → ② ONNXのような共通表現を経るか、あるいは直接 → ③ ターゲットバックエンド向けにコンパイルする、という流れです。NPUベンダーの多くもONNX入力を受け付けます。ただし今回のシリーズでは、ONNXを一枚かませる代わりに、PyTorchネイティブのグラフ(torch.export)をベンダーコンパイラに直接渡す経路を選びました — アーキテクチャが微妙に食い違うモデルを扱うときは、フォーマット変換を一段減らすほうがデバッグの表面積が小さかったからです。\nトレードオフ — 何を得て何を失うか eager(インタプリタ) コンパイル(翻訳書) 遅延 高くジッタ大 低く一定 柔軟性 任意のPython・動的shape OK 固定グラフ・固定shape デバッグ 簡単(値を即確認) 難しい(グラフが固まる) 準備コスト なし コンパイル時間が必要 まとめると、コンパイルとは**「柔軟なインタプリタ」を「決定的な実行バイナリ」に変える**ことです。柔軟性を差し出して、低遅延・低ジッタ・効率を得ます。\nでは、いつコンパイルするのか 研究・プロトタイピング、入力サイズが毎回変わる場合 → eagerで十分です。柔軟性に価値があります。 実サービスの低遅延推論、エッジ・オンデバイス、特化型アクセラレータ(NPU) → コンパイルが勝ちます。特にNPUは事実上コンパイルが必須です。 つまり「コンパイルが常に正解」ではありません。柔軟性が重要な局面ではeagerが、遅延・安定性・効率が重要な局面ではコンパイルが適しています。国産NPUに実際のモデルを載せながら、このトレードオフをどう乗り越えたかは移植実験記に続きます。\n","permalink":"http://blog.caveduck.io/ja/posts/neural-net-compilation/","summary":"「モデルをコンパイルする」とは正確にどういう意味なのでしょうか? eager実行との違いから、グラフキャプチャ・演算融合・カーネルlowering・静的shape・メモリプランニングまで、図とインタラクティブなデモで一つずつ解きほぐしていきます。","title":"ニューラルネットのコンパイルをやさしく理解する — PyTorchモデルがハードウェアの言語になるまで"},{"content":" この記事は軽量化シリーズの3本目（#3）です。データ型 → 量子化が「数値をより少ないビットで」だったとすれば、今回はいっそ接続そのものを刈り取るプルーニングです。コードはgithub.com/warpspaceinc/efficient-ml-practiceのpruning.ipynbノートブックにあり、以下のすべてのplotはそのコードをColab(A100)で実際に回して実測した私たちの数字です。\n1989年の「脳損傷」 プルーニングを語るとき、外せない論文が一つあります。タイトルが物騒です — Optimal Brain Damage(最適脳損傷)。著者はYann LeCun、John Denker、Sara Solla、AT\u0026amp;T Bell Labs、発表はNeurIPS 1989です。\n少し立ち止まって考えてみると驚きます。1989年。 今から36年前です。ワールド・ワイド・ウェブが世に出る前であり、LeCunがCNN(LeNet)で有名になる前であり、「ディープラーニング」という言葉すら存在しなかった時代です。そのLeCunは今やMetaのチーフAIサイエンティストであり、チューリング賞受賞者です。「ニューラルネットワークで重要度の低い接続を刈り取れば、より小さく、よりよく汎化するネットワークが得られる」 というプルーニングの核心的アイデアが、そんなに昔にすでに定式化されていたのです。\n論文の発想は生物学的な比喩ともつながっています。人間の脳もシナプスをやみくもに増やしません。ニューロンあたりのシナプス数は、乳児期に約2,500個 → 2〜4歳頃に15,000個まで急増し → 成人になると約7,000個へと再び減少します。 役に立たない接続を切り落とすこの「刈り込み(pruning)」こそが、かえって効率的な脳をつくるというわけです。\nこの記事では、その36年の洞察が今日NVIDIA GPUの2:4 sparsityとしてどのように現実になるのかを、3つの問いに沿って追いかけます。\n何を刈るのか (criterion) どれだけ刈れるのか (再学習と圧縮の驚き) どのように刈ればハードウェアが速くなるのか (granularityと2:4) プルーニングとは \u0026amp; 5段階フレームワーク プルーニングとは重みを0にして接続を削除することです。denseなネットワークをsparseにして、記憶・演算・電力を節約します。形式的には「非零の重みの数を$N$個以下に制限しつつ損失を最小化する」問題です。\n$$\\arg\\min_{W_P} L(x; W_P) \\quad \\text{s.t.} \\quad \\lVert W_P\\rVert_0 \u003c N$$Han et al.(NeurIPS 2015)以降、プルーニングは通常5段階に整理されます: ① 学習 → ② granularity(どのように) → ③ criterion(何を) → ④ ratio(どれだけ) → ⑤ fine-tune(回復). この記事ではそのうち核心である criterion → ratio/回復 → granularity の順に見ていきます。\n動機は量子化編と同じです。重みはたいてい0付近に集中しており、メモリアクセスは演算より数百倍高価です(Horowitz: DRAM 640pJ vs int ADD 0.1pJ)。だから「多くの重みがほぼ0なのに、本当にすべて必要なのか?」という問いが自然に湧いてきます。\n1. 何を刈るか — Criterion まず最初に答えるべき問いは「どの重みを消すのか」です。候補は3つあります。\nMagnitude(大きさ): 重要度 $= |w|$。小さければ消す。安くて単純。 OBD saliency(2次/ヘシアン): OBDの答えです。各重みを消したときに損失がどれだけ増えるかをテイラー展開で近似します。 Random: ランダム。比較用の下限。 OBDの導出はこうです。損失 $L$ を重みの変化 $\\delta w$ について2次まで展開すると:\n$$\\delta L = \\sum_i g_i\\,\\delta w_i + \\frac{1}{2}\\sum_i h_{ii}\\,\\delta w_i^2 + \\frac{1}{2}\\sum_{i\\neq j} h_{ij}\\,\\delta w_i \\delta w_j + O(\\lVert\\delta w\\rVert^3)$$ここで3つの仮定を入れます — 収束完了(1次項 $g_i \\approx 0$)、対角近似(交差項 $h_{ij}$ を無視)、2次近似(3次以上を無視)。すると、重み一つを0にするとき($\\delta w_i = -w_i$)の損失増加、すなわちsaliencyがきれいに残ります。\n$$\\text{saliency}_{w_i} = \\frac{1}{2}\\, h_{ii}\\, w_i^2$$大きさの大きい重み($w_i^2$)ほど、そして損失曲率が急なところ($h_{ii}$)ほど重要だという意味です。問題はヘシアン $h_{ii}$ が高価だということ — そこで実務では対角ヘシアンを**empirical Fisher(勾配の二乗の平均)**で近似します。私たちの実験もそうしました。\n実測: 3つの基準を比較 MNIST MLP(784→512→512→10)を学習したのち、3つの基準でそれぞれsparsityを上げていきながら(再学習なしで)精度を測りました。\n読み取るべき核心:\nrandomは50%を超えただけで崩壊します。 何を刈るかが決定的だという証拠です。 magnitudeとOBDはほぼ重なります。 どちらも90%まで精度を保ちます。極端(98%)ではOBDの2次の洞察が少し先行しますが(44.6% vs 29.3%)、その手前まではmagnitudeがむしろわずかに勝ります。 下で直接触ってみてください。スライダーでsparsityを上げると、切られたシナプスは右のニューラルネットから消え、接続が1つも残らないニューロンは除去されます。同時に演算量(MAC・FLOPs)も減ります — ただしこの削減は、ハードウェアが実際にゼロをスキップする場合にのみ実現します。基準をmagnitude ↔ randomに変えると、同じsparsity(同じMAC数)でも、どの接続・ニューロンが先に消えるかが変わります。\nこの結果こそが**「なぜ実務はOBDではなくmagnitudeを使うのか」**への答えです。OBDは理論的に優雅ですが、ヘシアンが高価です。一方でmagnitudeはタダなのに、これだけ強力です。だから36年経った今でも、ほとんどのプルーニングはmagnitudeベースです。OBDが開いた扉(「何を刈るかを原理的に選ぼう」)を、実際には最も単純な基準が実用的にくぐり抜けたわけです。\n2. どれだけ刈れるか — 再学習と圧縮の驚き 一度に大量に刈れば、当然精度は落ちます。ですが**刈って → 再学習(fine-tune)**を繰り返すと、話はまったく変わってきます。再学習の学習率は通常、元の1/10〜1/100と低く設定します。\nprune only(グレー): 再学習なしで刈ると95%で88%まで落ちます。 iterative prune+retrain(グリーン): 少しずつ刈りながら毎回再学習すれば、**重みを95%刈り取っても(=20分の1のパラメータ)、denseと事実上同じ精度(97.3% vs 97.6%)**を保ちます。 重みの20分の1しか残さなくても性能がそのままだという事実こそが、プルーニングが今なお研究され続ける理由です。プルーニング後の重み分布を見れば、何が起きたのか一目でわかります — ちょうど0に巨大なスパイクが立ちます。\nこの「刈って再び学習」というアイデアを極限まで推し進めたのがDeep Compression(Han et al., 2016)です。iterative pruningでAlexNetのプルーニング限界を5倍から9倍まで引き上げ、量子化・ハフマン符号化まで載せて最大数十倍を達成しました。\n3. どう刈れば速くなるか — Granularityとハードウェア ここで必ず押さえておくべき落とし穴があります。プルーニングでsparsityを得たからといって、勝手に速くなるわけではありません。\nじっくり考えてみると、重みを「刈る」というのは、実はその場所に0を入れることと変わりありません。そして0をそのまま乗算-累算器に入れて計算すると — $0 \\times a = 0$ — 演算回数は一つも減りません。 上で精度を測った実験も、実は重みを0にしたのちそのままdenseカーネルで回したもので、精度は測れましたが、記憶も速度もまったく減りませんでした(量子化編の「fake quantization」とまったく同じ状況です)。0を掛けようが0.3を掛けようが、乗算器の立場では同じ一回の乗算です。\n利益を得るには、0をそもそも記憶もせず、掛けもせずスキップしなければなりません。 ところがそれが難しいのです。ここまでのプルーニングは0がどこにでも散らばるunstructured(非構造)方式なので、圧縮率は最高でもGPUの立場では0の位置が不規則でスキップできません。 「次の0がどこにあるか」を毎回探すコストが、乗算一回を省くより高くつくので、結局denseで回すほうがマシになります。\nそこでプルーニングにはこの原則が付いてまわります:\nプルーニングは、ハードウェアがそのsparsityパターンを支援して初めて実利益が出る。使うハードウェアが何を加速するのかを知って刈らなければならない。\nこれまでsparse演算のための専用ハードウェア(EIE, ISCA 2016など)が研究されてきましたが、今日もっとも広く使われている答えがNVIDIAの2:4 structured sparsityです。\n2:4プルーニング 規則は単純です。連続する4個の重みごとに、ちょうど2個を0にします(50% sparsity)。0の位置が「4個中2個」と規則的なため、NVIDIA Ampere(A100)以降のSparse Tensor Coreがこのパターンをハードウェアで加速します。記憶も効率的です — 非零値の半分 + 各値の位置を指す2-bitインデックスさえあれば済みます。\nmagnitudeで4列ごとに小さい2個を刈るマスクは、こう作ります。\ndef two_four_mask(weight): \u0026#34;\u0026#34;\u0026#34;4列単位で|w|が大きい2個だけ残す2:4マスク。\u0026#34;\u0026#34;\u0026#34; w = weight.detach().abs(); R, C = w.shape mask = torch.zeros_like(weight) for j in range(0, C - C % 4, 4): idx = w[:, j:j+4].topk(2, dim=1).indices # 各4個のうち大きい2個 mask[:, j:j+4].scatter_(1, idx, 1.0) return mask 私たちのMLPに2:4を適用した結果: dense 97.6% → 2:4(再学習なし)96.8% → 2:4 + 再学習 98.2%。半分を規則的に刈っても、再学習すれば完全に回復します。\nPythonで2:4を活用する PyTorchは2:4パターンを圧縮保存し、Sparse Tensor Coreで掛けるAPIを提供します(fp16、Ampere+が必要)。\nfrom torch.sparse import to_sparse_semi_structured mask = torch.Tensor([0, 0, 1, 1]).tile((64, 16)).bool() W = (torch.rand(64, 64) * mask).half().cuda() Ws = to_sparse_semi_structured(W) # 非零値 + 2-bitインデックスで圧縮 x = torch.rand(64, 64).half().cuda() torch.mm(Ws, x) # Sparse Tensor Coreを使用 本当に速くなるか — 直接ベンチマーク A100でdense vs 2:4 sparseの行列積の速度をサイズ別に測りました。結果は正直で、示唆に富んでいます。\n小さい行列(N \u0026lt; 4096)では2:4がむしろ遅いです。 圧縮・インデックシングのオーバーヘッドが利益より大きいのです。 大きい行列でのみ利益が出ます。 N=8192で約1.72倍速くなります。 これが「ハードウェアとワークロードを知らなければならない」の実体です。同じ2:4プルーニングでも、Ampere以上のGPUで、十分に大きい行列でのみ速くなります。Turing(T4)や小さい行列では利益がないか、むしろ損です。プルーニングはアルゴリズムだけの問題ではなく、最初から最後までハードウェアとともに設計しなければならない問題です。\n4. プルーニング × 量子化は直交する — Deep Compression ここまでプルーニングを見てきましたが、先の量子化編ではビットを削りました。核心はこの二つが互いに直交(orthogonal)するということです。プルーニングは「どの重みを消すか」を、量子化は「残った重みを何ビットで表現するか」を決めます。軸が違うので一緒に使えば利益が掛け算されます。\nこれを最もよく示したのがDeep Compression(Han et al., 2016)の3段階パイプラインです。\nプルーニング + 再学習 — 弱い接続を消し、再び学習して回復。 量子化 + 再学習 — 生き残った重みをK-Meansコードブック数個にまとめ(コードブックを再学習)、インデックスだけを保存。 ハフマン符号化 — インデックス分布が片側に偏っているので、無損失でもう一度圧縮。 私たちのMLPで実際に回してみました。各段階でfc2の重み分布がどう変わるかを見ると、プルーニングと量子化が分布の異なる部分を触っていることが一目でわかります。(0になった重みは隠し、生き残ったものだけを描きました。)\n① dense — おなじみの釣鐘型の正規分布。 ② プルーニング(80%) — 0近傍の小さな重みが丸ごと消えます。真ん中が吹き飛び、両側の裾だけが残ります。 ③ 再学習 — 生き残った重みがデータに再び合わせられ、はっきりとした2モード(bimodal)分布になります。 ④ 量子化(4-bit) — その連続分布が16個の離散レベルに畳み込まれます(ここが[量子化編]のK-Meansがやること)。 プルーニングは横軸の真ん中(小さい値)を空にし、量子化は残った値をいくつかの縦線に離散化します。触る場所が違うので重ならずに積み上がります。その結果、モデルサイズは段階ごとに掛け算で縮みます。\ndense 2,612 KB → プルーニング 604 KB (4.3×) → +量子化 147 KB (17.8×) → +ハフマン 141 KB (18.6×) その間、精度はむしろ 97.5% → 98.3% へと維持・改善されました(再学習のおかげ)。 つまりプルーニングだけで4.3×、量子化を載せて17.8×、ハフマンまでで18.6×。各手法の利益は足されるのではなく掛け算されます。 原論文はこのパイプラインでAlexNetを35×、VGG-16を49×縮めました(精度を落とさずに)。データ型 → 量子化 → プルーニング、このシリーズが扱った3つの軸は、結局一つのモデルに同時に載せられる直交したツールなのです。\n自分で回してみる 上のすべてのplotは、以下のノートブックをColabで実行して出たものです。ランタイム → すべて実行でOKです(2:4速度測定はA100などAmpere GPUを選択)。\n📓 pruning.ipynb — OBD vs magnitude、再学習圧縮、2:4 sparsity リポジトリ: github.com/warpspaceinc/efficient-ml-practice まとめ 何を: randomはすぐに崩壊する。magnitude(|w|)は安くて強力で、OBDの2次saliencyに匹敵する — だから実務の標準になった。 どれだけ: prune→retrainを繰り返せば、95%(20倍)を刈っても精度が維持される。 どのように: unstructuredは圧縮率だけが高い。2:4 structuredでこそAmpere Sparse Tensor Coreが実際に加速する — ただし、大きい行列でのみ。 一緒に: プルーニングと量子化は直交する。Deep Compressionのように積み重ねれば利益が掛け算される(私たちの実測で18.6×、精度維持)。 Optimal Brain Damageが1989年に蒔いた洞察は、今なお有効です。ただしそれが「実利益」になるためには、36年後のハードウェアがそのsparsityを受け止めてくれる必要がありました。プルーニングの歴史は、すなわちアルゴリズムとハードウェアが互いを待ち続けてきた歴史でもあるのです。\n次回は、sparsityを層ごとに自動的に決める方法(sensitivity analysis・AMC・NetAdapt)を扱っていきます。\nReferences Y. LeCun, J. S. Denker, S. A. Solla. Optimal Brain Damage. Advances in Neural Information Processing Systems (NeurIPS), 1989. S. Han, J. Pool, J. Tran, W. J. Dally. Learning both Weights and Connections for Efficient Neural Networks. NeurIPS, 2015. S. Han, H. Mao, W. J. Dally. Deep Compression: Compressing Deep Neural Networks with Pruning, Trained Quantization and Huffman Coding. ICLR, 2016. A. Mishra et al. Accelerating Sparse Deep Neural Networks (2:4 Structured Sparsity). arXiv:2104.08378, 2021. S. Han et al. EIE: Efficient Inference Engine on Compressed Deep Neural Network. ISCA, 2016. M. Horowitz. Computing\u0026rsquo;s Energy Problem (and what we can do about it). ISSCC, 2014. 講義参考: MIT 6.5940 TinyML and Efficient Deep Learning Computing (Song Han), Lec 3–4 Pruning \u0026amp; Sparsity. ","permalink":"http://blog.caveduck.io/ja/posts/neural-net-pruning/","summary":"1989年、LeCunのOptimal Brain Damageから始まる、重みを刈り込んで(pruning)モデルを小さくする物語。何を・どうやって・どれだけ削るのか、再学習すれば95%削ってもびくともしない圧縮の驚き、そしてハードウェア(NVIDIA 2:4)が支えて初めて実利益が出るところまで — MNIST MLPで実測したplotとともに。","title":"軽量化シリーズ #3 — ニューラルネット・プルーニング"},{"content":" この記事はディープラーニング時代のデータ型編の続編です。前回の記事でINT/FPのデータ型がビットをどう数値として解釈するかを見たなら、この記事ではその低ビット型を使って実際に学習済みモデルを削り、結果を測定します。コードはすべて github.com/warpspaceinc/efficient-ml-practice のノートブックから持ってきたもので、以下のplotはそのコードをColab（T4）でそのまま回して出力したものです。\n32ビットの数値は本当にすべて必要なのか 前回の記事の最後で観察したことが一つありました。ニューラルネットワークの重みは、たいてい0の近くに釣鐘型で密集していて、裾が長いということです。実際にMNIST用の小さなMLP（784→256→128→10）をわずか2エポックだけ学習させたあと、第1層の重みをヒストグラムに描いてみると、まさにその通りになります。\n値がこれほど狭い範囲に密集しているのに、本当に一つ一つを32ビットの実数として個別に保存し、32ビットで掛け算する必要があるのでしょうか。量子化（quantization） は「いいえ」と答えます。この記事では、代表的な2つの流れをコードで直接回してみます。\nK-Means量子化 — 近い値を代表値でまとめて保存を減らす（非均等）。 Linear量子化 — 整数格子に写像して保存も演算も整数にする（均等）。 どちらの方式も学習が終わったモデルに事後適用する PTQ（post-training quantization） で、ここでは重みだけを量子化します。\n1. K-Means量子化 — 代表値の共有で保存を減らす 最初のアイデアは2016年のDeep Compression論文1から来ました。近い値の重みを一つの塊にまとめて、代表値一つで済ませようというものです。たとえば 2.09, 1.92, 1.87 はすべて「だいたい2.0」なので、これらの値を 2.00 一つに置き換え、各位置には「何番目の代表値を使うか」というインデックスだけを記録しておきます。\n手順は3ステップです。\nクラスタリング — すべての重みを $k = 2^N$ 個のグループにK-Meansクラスタリング。 コードブック（codebook） — 各グループの中心値（centroid）を代表値として保存。このリストがコードブック。 インデックス — 各位置には32ビットの実数の代わりに「何番目のクラスタか」を指す $N$ ビットの整数だけを保存。 コードではscikit-learnの KMeans をそのまま使います。局所最適解を避けるため、min〜maxを均等分割した値で初期化します。\nfrom sklearn.cluster import KMeans import numpy as np def kmeans_quantize(w, n_bits, fit_sample=20000): \u0026#34;\u0026#34;\u0026#34;重みをK-Meansで量子化。復元値、コードブック、インデックスを返す。\u0026#34;\u0026#34;\u0026#34; shape = w.shape flat = w.reshape(-1, 1) k = 2 ** n_bits lo, hi = float(flat.min()), float(flat.max()) init = np.linspace(lo, hi, k).reshape(-1, 1) # 線形初期化 idx = np.random.choice(len(flat), min(fit_sample, len(flat)), replace=False) km = KMeans(n_clusters=k, init=init, n_init=1, max_iter=50).fit(flat[idx]) codebook = km.cluster_centers_.reshape(-1) # 代表値リスト indices = km.predict(flat) # 各重み → クラスタインデックス recon = codebook[indices].reshape(shape) # 復元 return recon, codebook, indices 2ビット（$k=4$）で回した結果を元の分布の上に重ねて描くと、代表値（縦線）が値の密集する0付近に細かく配置されるのが分かります。これが「非均等（non-uniform）」の意味です。\nどれくらい減るか 圧縮率はビットの会計で計算します。パラメータ $M$ 個、$N$ ビット量子化なら:\n$$\\text{元} = 32M \\text{ bit}, \\qquad \\text{圧縮} = \\underbrace{N \\cdot M}_{\\text{インデックス}} + \\underbrace{32 \\cdot 2^N}_{\\text{コードブック}} \\text{ bit}$$num = w.size; k = 2 ** N_BITS orig_bits = 32 * num comp_bits = N_BITS * num + 32 * k # インデックス + コードブック print(f\u0026#34;compression: {orig_bits/comp_bits:.2f}x\u0026#34;) $M \\gg 2^N$ ならコードブック項は無視できるようになり、圧縮率は $32M/NM = \\mathbf{32/N}$ 倍へ収束します。2ビットなら約16倍、4ビットなら8倍です。（第1層fc1を基準にした実測圧縮率: 15.99×）\n重要な限界: 保存しか減らない ここは必ず押さえておくべきです。K-Means量子化が減らすのはディスク・メモリに保存されるサイズだけです。推論するときは、インデックスをコードブックでデコードして元の実数の重みを復元したうえで掛け算しなければなりません。\n保存: 整数インデックス（小さい）✓ 演算: 復元された32ビット実数で依然としてfloating-point演算 ✗ メモリ帯域は節約しましたが、乗算器（MAC）は依然としてfloatを回します。本当に整数演算まで速くするには、次の方式が必要です。\n2. Linear量子化 — 整数演算だけで推論する Linear（線形）量子化は、整数と実数をアフィン（affine）写像一行でつなぎます。2018年にJacob et al.2が提案し、TensorFlow LiteのINT8量子化がまさにこの方式です。\n$$r = S \\cdot (q - Z)$$ $r$ — 元の実数（real）値 $q$ — 量子化された整数 $Z$ — zero point。実数 $0$ にちょうど対応する整数。ReLUの後の0やパディングのようなよくある値を誤差なく表現するための仕組み。 $S$ — scale。整数一目盛りが実数でいくらか（floating-point）。 $S$ と $Z$ は、実数の範囲 $[r_{\\min}, r_{\\max}]$ の両端を整数の範囲 $[q_{\\min}, q_{\\max}]$ の両端に合わせるところから出てきます。\n$$S = \\frac{r_{\\max} - r_{\\min}}{q_{\\max} - q_{\\min}}, \\qquad Z = \\text{round}\\left(q_{\\min} - \\frac{r_{\\min}}{S}\\right)$$コードでは、この $S$、$Z$ さえ計算してやれば、量子化（round・clamp）と復元（dequant）はPyTorch組み込みの fake_quantize_per_tensor_affine が処理します。任意のビット幅は quant_min/quant_max で指定します。\nimport torch def linear_quantize(w, n_bits, symmetric=False): \u0026#34;\u0026#34;\u0026#34;PyTorch組み込みのfake_quantizeで線形（アフィン）量子化。復元値とS, Zを返す。\u0026#34;\u0026#34;\u0026#34; t = torch.as_tensor(w, dtype=torch.float32) if symmetric: # Z=0固定（演算が単純） qmax = 2 ** (n_bits - 1) - 1; qmin = -qmax S = float(t.abs().max()) / qmax; Z = 0 else: # asymmetric: 範囲を正確に合わせる qmin = -2 ** (n_bits - 1); qmax = 2 ** (n_bits - 1) - 1 S = (float(t.max()) - float(t.min())) / (qmax - qmin) Z = int(round(qmin - float(t.min()) / S)) recon = torch.fake_quantize_per_tensor_affine(t, S, Z, qmin, qmax) return recon.numpy(), S, Z 2ビットで回すと、代表値（縦線）が一定の間隔 $S$ で均等に配置されます。K-Meansとちょうど対比される点です — こちらはデータがどこに密集していようと格子が均一です。\nなぜ「整数演算まで」可能なのか Linear量子化の本当の値打ちは保存ではなく演算にあります。行列積 $Y = WX$ の各値をアフィン写像に置き換えると:\n$$q_Y = \\frac{S_W S_X}{S_Y}\\big(q_W q_X - Z_W q_X - Z_X q_W + Z_W Z_X\\big) + Z_Y$$括弧の中は $q_W q_X$ を含めてすべて整数の掛け算・足し算で、$Z_W Z_X$ のように入力に依存しない項はあらかじめ計算しておけます。前のスケール比 $\\frac{S_W S_X}{S_Y}$ だけが実数ですが、この値は経験的に常に $(0,1)$ の区間にあるので、$2^{-n} M_0$ の形の固定小数点の掛け算 + ビットシフトで処理されます。結局floating-pointユニットはまったく必要ありません — 第1編のエネルギー表で見た「整数演算はfloatより数十倍安い」がここで現実になります。\n重みはたいてい0対称なので、実戦では $Z_W = 0$ である対称（symmetric）量子化をよく使います。そうすると $Z_W$ 関連の項がまるごと消え、式がさらにすっきりします。\n3. で、どれくらい失うのか — 実際に測定 言葉だけでは掴めないので、3つの方式（Linear asymmetric / Linear symmetric / K-Means）をビット幅を変えながらMNISTのテスト精度で比較しました。モデルの3つのLinear層の重みをすべて量子化したうえで（復元値に置き換え）精度を測ったものです。\ndef eval_linear(model, n_bits, symmetric): m = copy.deepcopy(model) with torch.no_grad(): for layer in [m.fc1, m.fc2, m.fc3]: wl = layer.weight.detach().cpu().numpy() recon, *_ = linear_quantize(wl, n_bits, symmetric=symmetric) layer.weight.copy_(torch.tensor(recon, dtype=torch.float32, device=device)) return accuracy(m) bits_list = [2, 3, 4, 8] acc_asym = [eval_linear(model, b, False) for b in bits_list] acc_sym = [eval_linear(model, b, True) for b in bits_list] # K-Means も同じ構造の eval_kmeans(model, b) で比較 bits Linear (asym) Linear (sym) K-Means float32 8 97.2% 97.2% 97.2% 97.2% 4 97.1% 97.0% 97.1% 97.2% 3 96.7% 94.4% 96.8% 97.2% 2 58.1% 18.8% 93.9% 97.2% 読み取るべき大きな絵はこうです。\n8ビットなら事実上タダ。 3つの方式すべてがfloat32と区別がつかないほど精度を保ちます。 低ビットに行くほどK-Meansが有利。 代表値をデータ分布に合わせて（非均等）配置するため、同じ2ビットでも均等格子のLinearより誤差が小さくなります。下のMSE曲線でより顕著です。 しかし精度がすべてではありません。K-Meansが2ビットで誤差が小さくても、推論の演算は依然としてfloatです。Linearは誤差を少し多めに引き受ける代わりに、演算まで整数にして実際のハードウェアで速くなります。2つの方式は競合ではなく、目的の異なる道具です。\n方式 保存 演算 格子 元 FP重み FP算術 — K-Means量子化 整数インデックス + FPコードブック FP算術 非均等 Linear量子化 整数重み 整数算術 均等 実際に回してみる 上のplotと表はすべて、以下の2つのノートブックをColabで実行して出てきたものです。ランタイム → すべて実行 で終わり、自分のモデルの .safetensors をアップロードしてパスだけ変えればそのまま適用できます。\n📓 linear-quantization.ipynb — アフィン写像、symmetric/asymmetric 📓 kmeans-quantization.ipynb — コードブック/インデックス、圧縮率 まとめ 量子化 = 表現可能な値の個数を減らすこと。 重みは0付近に密集しているので、32ビットを丸ごと使うのは無駄です。 K-Means（非均等） は代表値を分布に合わせて配置し保存を減らすが、演算はfloat。低ビットで誤差が小さいです。 Linear（均等） はアフィン写像 $r=S(q-Z)$ で保存も演算も整数化。ハードウェア高速化の基盤です。 8ビットはほぼタダ、2ビットまで押し込むと方式の選択が精度を分けます。 次の実験ノートでは、ここで扱いきれなかった活性値（activation）量子化と QAT（量子化を意識した学習） で、整数推論パイプラインを最後まで組み上げてみます。\nHan, Mao, Dally. Deep Compression: Compressing Deep Neural Networks with Pruning, Trained Quantization and Huffman Coding. ICLR 2016.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nJacob et al. Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference. CVPR 2018.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"http://blog.caveduck.io/ja/posts/neural-net-quantization/","summary":"学習済みの32ビットニューラルネットワークの重みを、K-Means（非均等）とLinear（均等・整数演算）の2つの方式で2〜8ビットまで削ってみます。アフィン写像 r=S(q−Z) から圧縮率・精度のトレードオフまで、MNIST MLPで実際にコードを回して測定したplotとともに。","title":"軽量化シリーズ #2 — ニューラルネット量子化"},{"content":" WarpspaceはCaveduck.ioをオンデバイス・低コストで動かすために、モデルを小さくするさまざまな実験を行っています。この記事はその軽量化シリーズの第1回（#1 データ型）です。続く#2 量子化・#3 プルーニング編では、ここで扱ったデータ型を使って実際にモデルを小さくし、その結果を自分の手で測定します。\nデータ型はなぜこんなに増えたのか 数年前まで、ディープラーニングの数値はただfloat32一つで十分でした。ところが最近の論文やモデルカードを見ると、FP16、BF16、FP8 (E4M3/E5M2)、INT8、INT4、FP4、NF4… ありとあらゆるデータ型があふれています。名前を見ただけでは何が何やら、なぜこんなに種類があるのか分かりません。\n理由は単純です。低ビット演算はとにかく安いからです。45nmプロセスを基準にすると、演算一つあたりのエネルギーコストはおおよそ次のとおりです。\n演算 エネルギー (pJ) 8-bit int ADD 0.03 32-bit int ADD 0.1 32-bit float ADD 0.9 8-bit int MULT 0.2 32-bit float MULT 3.7 8ビット整数の乗算は32ビットfloatの乗算より約18倍安く、加算は30倍安いです。さらに、メモリから値を一つ読み出すコストは、乗算・加算一回の数百倍も高くつきます。ですから同じモデルであっても、数値をより少ないビットに収めるほど電力も、メモリも、帯域幅も節約できます。上に並んだ多種多様なデータ型は、結局のところ「何ビットを、どこに使うのか」という問いに、それぞれ異なる答えを出した結果なのです。\n問題は、ビットを減らすと表現できる数値の種類が減り、その分だけ精度や範囲を失うということです。そのため、フォーマットごとに符号・指数・仮数へビットをどう配分するかで性格が分かれます。この記事では、その配分ルールを — ビットが実際にどんな数値へ解釈されるのか — を一つずつ紐解いていきます。\n各データ型ごとに、ビットを直接クリックできるウィジェットを用意しました。0と1を押してひっくり返すと、計算式と結果値がリアルタイムに変わります。説明を目で読むよりも、自分でビットを触ってみるほうが、はるかに早く感覚がつかめます。\n1. 整数 (Integer) 符号なし整数 (Unsigned Integer) 最も単純です。$n$個のビットそれぞれが$2^k$の桁の値を持ち、立っているビット($=1$)の桁の値をすべて足し合わせます。\n$$\\text{value} = \\sum_{i=0}^{n-1} b_i \\cdot 2^i$$表現範囲は$[0,\\ 2^n - 1]$。8ビットなら0から255までです。\n下のウィジェットでビットを押してみてください。デフォルト値00110001は$2^5 + 2^4 + 2^0 = 49$です。\n符号あり整数 (Signed Integer) 負の数を表現するには符号が必要です。二つの方式があります。\n符号・絶対値 (Sign-Magnitude) — 先頭のビットを符号として使い($0$=正、$1$=負)、残りで大きさを表現します。直感的ですが致命的な欠点があります。00000000と10000000がどちらも0です(+0と−0)。0が二つあるとハードウェアが煩雑になります。範囲は$[-2^{n-1}+1,\\ 2^{n-1}-1]$。\n2の補数 (Two\u0026rsquo;s Complement) — 現代のコンピュータが実際に使っている方式です。アイデアは簡単です。先頭ビットの桁の値を負にします。つまりMSBの重みが$+2^{n-1}$ではなく$-2^{n-1}$になります。\n$$\\text{value} = -b_{n-1}\\cdot 2^{n-1} + \\sum_{i=0}^{n-2} b_i \\cdot 2^i$$こうすると0は00000000一つだけになり、範囲は$[-2^{n-1},\\ 2^{n-1}-1]$と負の側が一つ広くなります。下の11001111は$-2^7 + 2^6 + 2^3 + 2^2 + 2^1 + 2^0 = -49$です。MSBを消すとどう変わるか押してみてください。\n2. 固定小数点 (Fixed-Point) 整数だけでは小数を収められません。最も簡単な拡張は、小数点の位置を固定しておくことです。ビット列をそのまま2の補数の整数として読み取り、定めた分だけ$2^{-f}$でスケールします。\n$$\\text{value} = (\\text{整数として読んだ値}) \\times 2^{-f}$$下は8ビットを整数部4ビット・小数部4ビット($f=4$)に分けたものです。各ビットの重みが$2^3, 2^2, \\dots, 2^0, 2^{-1}, \\dots, 2^{-4}$と続きます。00110001は整数として読むと49で、$49 \\times 2^{-4} = 3.0625$です。\n固定小数点は単純ですが限界がはっきりしています。小数点の位置が固定なので、表現できる大きさの幅(range)が狭いです。 非常に大きな数と非常に小さな数を同時に扱うのが困難です。この問題を解決するのが、次の主役、浮動小数点です。\n3. 浮動小数点 (Floating-Point) — IEEE 754 浮動小数点は名前のとおり、小数点が浮遊します。 科学的記数法($1.5 \\times 10^3$)の2進数バージョンだと考えればよいでしょう。ビットを三つの部分に分けます。\nSign (符号) — 1ビット Exponent (指数) — 大きさの規模を定める → 表現範囲(range) を決定 Fraction (仮数部) — 細かい値を定める → 精度(precision) を決定 一般的な(normal)数の解釈式は次のとおりです。\n$$\\text{value} = (-1)^{\\text{sign}} \\times (1 + \\text{Fraction}) \\times 2^{\\text{Exponent} - \\text{bias}}$$ここでbiasは指数を負の値まで表現するためのオフセットで、$2^{e-1}-1$($e$=指数のビット数)です。そして仮数の前の**$(1 + \\cdots)$** に注目してください。正規数は常に先頭に暗黙の1が付きます(これを「implicit leading 1」と呼びます)。\nちょっと用語整理 — 仮数 / Fraction / Mantissa / Significand\nこの部分の用語は文献ごとに少しずつ混ざって使われるので混乱しやすいです。上の式の$1.\\text{Fraction}$全体、つまり有効数字の部分を指す正式名称はSignificand(有効数字) です。\nFraction (仮数部) — ビットに実際に格納される小数部分だけを指します。ウィジェットで黄色く塗られたビットがまさにこれ。小数点以下の値$0.\\text{b}_1\\text{b}_2\\cdots$です。 Significand (有効数字) — 暗黙の1まで含めた**$1.\\text{Fraction}$全体**。実際に掛けられる有効数字です。 Mantissa (仮数) — 歴史的には対数表の小数部分を呼んだ言葉ですが、浮動小数点では普通Fractionと同じ意味でゆるく使われます。IEEE 754標準そのものは「Significand」を正式用語とし「mantissa」は推奨しませんが、現場では依然として「仮数/mantissa」がよく使われます。 まとめるとSignificand = 1 + Fraction、そして日常的に「仮数(mantissa)」と言えば、たいてい格納されるFractionを指します。この記事ではビットに収まるフィールドをFraction(仮数部) と呼びます。\n下は32ビット単精度(FP32)です。符号1 + 指数8 + 仮数23 = 32ビット、biasは127。デフォルト値は$0.265625 = (1 + 0.0625) \\times 2^{125-127}$を表現します。指数ビットを一つずつ押して、値が2倍ずつ跳ねるのを確認してみてください。\n特別な値たち: 0、無限大、NaN、そしてsubnormal 式を見ると奇妙な点が一つあります。$(1 + \\text{Fraction})$のせいで、正規数では0を表現できません。 そこでIEEE 754は指数フィールドを特別な信号として使います。\n指数(Exponent) Fraction = 0 Fraction ≠ 0 解釈 00…0 (=0) $\\pm 0$ subnormal $(-1)^s \\times \\text{Fraction} \\times 2^{1-\\text{bias}}$ 00…1 ~ 11…0 normal normal $(-1)^s \\times (1+\\text{Fraction}) \\times 2^{\\text{Exp}-\\text{bias}}$ 11…1 (=max) $\\pm\\infty$ NaN — 肝心なのは指数がすべて0のときです。このときは暗黙の1を外し($1+\\text{Fraction} \\to \\text{Fraction}$)、指数を$2^{1-\\text{bias}}$に固定します。こうして作られるのがsubnormal(非正規)数で、0付近の非常に小さな値を細かく埋めてくれます。逆に指数がすべて1のときは無限大(仮数0)とNaN(仮数 ≠ 0)になります。\n上のFP32ウィジェットで実際に作ってみてください。\n指数をすべて1に (0 11111111 0…0) → +∞ そこから仮数のどれかのビットを立てると → NaN すべて0に → 0 指数だけ0にしたまま仮数を少し立てると → 非常に小さなsubnormal値 指数の幅が広いほど表現範囲が広がり、仮数の幅が広いほど精度が上がります。Exponent → Range、Fraction → Precision。 この一行が、以降すべての低精度フォーマット設計における核心的なトレードオフです。\n4. 半分のサイズへ: FP16とBF16 FP32は正確ですが32ビットも食います。ディープラーニングはそこまで精密である必要がない場合が多いので、16ビットフォーマットを使います。同じ16ビットなのに、ビットをどこに配分するかで分かれます。\nFP16 (IEEE 754 Half Precision) 指数5 + 仮数10。biasは15。精度(仮数)に多く投資した配分です。下は$1\\,10001\\,1100000000$、つまり$-(1+0.75)\\times 2^{17-15} = -7.0$です。\nBF16 (Google Brain Float) 指数8 + 仮数7。biasは127。FP16と総ビット数は同じですが、指数をFP32とまったく同じ8ビットに保ちます。つまり表現範囲はFP32と同一で、その代わりに精度を犠牲にしました。学習中の勾配のように値の規模(scale)が大きく揺れ動く状況でもオーバーフローの心配がないため、人気があります。\n下は$2.5 = (1 + 0.25)\\times 2^{1}$をBF16で表現したものです($0\\,10000000\\,0100000$)。\n二つのウィジェットで指数ビット数の違いを感じてみてください。BF16は指数のセルが8マスあるので非常に大きい/小さい数まで届きますが、仮数が7マスしかないため値がまばらです。FP16はその逆です。\n5. さらに下へ: FP8とFP4 FP8 (E4M3 / E5M2) 8ビット浮動小数点は最新のハードウェア(例: Nvidia Hopper/Blackwell)がサポートしています。二つの配分が標準のように使われます。\nE4M3 — 指数4 + 仮数3。精度優先。順伝播(forward)の重み・活性値に主に使います。INFがなくS.1111.111だけをNaNとして使います。表現可能な最大の正規値は$448$。 E5M2 — 指数5 + 仮数2。範囲優先。逆伝播(backward)の勾配のように規模の大きな値に使います。IEEEのようにINFとNaNを持ちます。 まずE4M3から。biasは7です。デフォルト値0 0111 000は$ (1+0)\\times 2^{7-7} = 1.0$です。\nE5M2。bias 15。指数5マスでずっと広い範囲を収めますが、仮数が2マスしかないため値の間隔が粗いです。\nINT4とFP4 最も極端です。4ビットで表現可能な値はわずか16個だけです。この16個をどう配置するかがフォーマットごとに異なります。\nINT4 — 2の補数の整数。$-8$から$7$まで均等な間隔で並びます。\nFP4は指数/仮数の配分によって値の分布が変わります。指数ビットを増やすほど0付近は密に、外側は粗く、非均等(non-uniform) に広がります。\nE1M2 — 指数1 + 仮数2。整数に最も近いです (bias 0)。0111 = $(1+0.75)\\times 2^{1-0} = 3.5$。 E2M1 — 指数2 + 仮数1 (bias 1)。0111 = $(1+0.5)\\times 2^{3-1} = 6$。 E3M0 — 指数3 + 仮数0 (bias 3)。仮数がないため事実上2の冪だけを表現します。0111 = $(1+0)\\times 2^{7-3} = 16$。randomボタンを押して、値が$\\dots, 4, 8, 16$のように指数的に広がっていくのを確認してみてください。 この四つのフォーマットの「表現可能な16個の値」を数直線の上に一度に打ってみると、性格の違いが一目で分かります。INT4は一定間隔、FP4は指数ビットを増やすほど0付近が密になり外側が広がります。\nここで面白い観察が一つ出てきます。ニューラルネットの重みの分布は、たいてい0付近に集中していて裾が長いです。 だとすれば、値を均等に並べるINTよりも、0付近を密に埋めるFP系のほうが、同じ4ビットでも実際の値をより上手く合わせられます。どんな分布にはどんなフォーマットが合うのか — データ型の選択がそのまま精度につながる理由です。(この観察を実際のモデルで測定するのは量子化編で扱います。)\nまとめ ここまでが、ディープラーニングで出会うデータ型が実際に何を意味するのかについての話です。整理すると、\n整数 / 固定小数点 — 値が均等な間隔。単純だが範囲が狭い。 浮動小数点 — 指数で範囲を、仮数で精度を分け合う。0付近は密で外側は粗い。 ビットを減らすということ = 表現可能な値の個数を減らすということ。FP32の約43億個から、FP4の16個まで。 これでFP16、BF16、FP8 E4M3、INT4のような名前を見れば、頭の中にビット配置が描けるようになるでしょう。ディープラーニングにおける「どのデータ型を使うか?」は、もはや些細な実装ディテールではなく、モデルをどれだけ小さく速く、それでいて正確に動かせるかを分ける設計の選択になったのです。\n次の記事では、これらのデータ型を武器に、すでに学習済みの32ビットモデルを実際に2〜8ビットへ小さくしてみて — 精度がどれだけ保たれるのか、サイズがどれだけ減るのか — 自分でコードを走らせて測定します。\n👉 ニューラルネット量子化編: 32ビットモデルを2ビットまで小さくしてみる\n","permalink":"http://blog.caveduck.io/ja/posts/deep-learning-data-types/","summary":"INT8、FP16、BF16、FP8(E4M3/E5M2)、FP4 — ディープラーニングのモデルカードにあふれるデータ型が実際に何を意味するのか、ビットがどのように数値へ解釈されるのかを一つずつ紐解きます。ビットを直接クリックすると計算式と値がリアルタイムに変わるウィジェットとともに。","title":"軽量化シリーズ #1 — ディープラーニングのデータ型"},{"content":"こんにちは！ Warpspace ブログへようこそ！AIコンパニオンサービス Caveduck.io を作る旅、インサイト、経験を共有するこの場を開設できて嬉しく思います。\nブログでお届けする内容 技術ディープダイブ: Caveduck を支える技術の舞台裏 プロダクトアップデート: 新機能、改善点、ロードマップのお知らせ AI \u0026amp; テクノロジートレンド: 進化するAI分野への私たちの視点 チームストーリー: AIコンパニオンの未来を作る人々 つながりましょう ソーシャルメディアをフォローし、RSSフィードを購読して最新記事をチェックしてください。\n皆さんと一緒にできることを嬉しく思います。一緒に未来を作りましょう！\n- Warpspace チーム\n","permalink":"http://blog.caveduck.io/ja/posts/welcome/","summary":"Warpspace ブログのご紹介 - Caveduck.io を作る旅をお伝えします","title":"Warpspace ブログへようこそ"},{"content":"Warpspace Inc. について Warpspace Inc.は、意味のある没入感のあるインタラクションを提供する革新的なAIコンパニオンサービス Caveduck.io を開発している会社です。\n私たちのミッション 私たちは、役立ち、安全で、心から楽しめるAIコンパニオンを作ることを信じています。最高水準の品質とユーザー体験を維持しながら、AI-人間インタラクションの可能性を広げていくことが私たちの目標です。\nCaveduck.io Caveduck.ioは私たちの主力サービスです - パーソナライズされた没入感のある会話を提供するAIコンパニオンサービスです。クリエイティブなパートナー、学習の相棒、または単に話し相手を探しているなら、Caveduckがそばにいます。\nお問い合わせ メール: contact@caveduck.io ウェブサイト: caveduck.io 会社ホームページ: warpspace.caveduck.io GitHub: github.com/warpspaceinc ","permalink":"http://blog.caveduck.io/ja/page/about/","summary":"Warpspace Inc.とCaveduck.ioについて","title":"について"}]