0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

safetensorsってなんだ?〜AIモデルの「安全な保存形式」を完全理解する〜

0
Posted at

この記事の対象読者

  • HuggingFaceからモデルをダウンロードしたことがある方
  • .safetensors ファイルを見かけるけど、.bin と何が違うのかわからない方
  • Stable DiffusionやLLMのモデルファイルを扱っている方
  • 「pickleは危険」と聞いたけど何が危険なのか具体的に知らない方

この記事で得られること

  • safetensorsの正体: なぜ「安全」と名乗るのか、pickle形式の具体的な危険性を理解できる
  • 内部構造の理解: バイナリフォーマットの仕組みを知り、「ブラックボックス」を脱却できる
  • 実践スキル: safetensorsの読み書き、既存モデルの変換方法を身につけられる

この記事で扱わないこと

  • GGUFやONNXなど他のモデル形式との網羅的な比較
  • モデルの学習・ファインチューニング手法そのもの
  • HuggingFace Hubの使い方全般

1. safetensorsとの出会い

HuggingFaceでモデルをダウンロードしていると、ある時期を境にファイル拡張子が変わったことに気づいた人はいるだろうか。

以前は pytorch_model.bin だったのが、いつの間にか model.safetensors になっている。

「なんか新しい形式になったな」程度に思っていた私は、ある記事を読んで背筋が凍った。

Pythonのpickle形式には、ファイルを読み込んだだけで任意のコードが実行される脆弱性がある。

そう、pytorch_model.bin の正体はpickleだ。つまり、悪意のあるモデルファイルをダウンロードして torch.load() した瞬間に、あなたのPCで攻撃者のコードが走る可能性があったのだ。

safetensorsは、この「AIモデル配布における信頼の問題」を根本から解決するために生まれたフォーマットだ。HuggingFaceが開発し、2023年頃から急速に普及。2026年現在、500万人以上のユーザーが利用するHuggingFaceエコシステムの事実上の標準形式になっている。

ここまでで、safetensorsが「単なる新しいファイル形式」ではなく、セキュリティ上の必要性から生まれたものだと伝わっただろうか。
次は、この記事で使う用語を整理しておこう。


2. 前提知識の確認

2.1 pickle(ピクルス)とは

Pythonのオブジェクトをバイト列に変換(シリアライズ)して保存・復元するための標準ライブラリ。漬物(pickle)にたとえて名付けられた。便利だが、復元時に __reduce__ メソッドを通じて任意のPythonコードを実行できてしまうという致命的な設計上の問題がある。

2.2 ゼロコピー(Zero-copy)とは

データを読み込む際に、メモリ上でのコピーを最小限に抑える技術。通常、ファイル→バッファ→目的の場所、と2回コピーが発生するところを、ファイルから直接目的の場所に読み込む。大きなモデルファイルの読み込みが劇的に速くなる。

2.3 テンソル(Tensor)とは

ニューラルネットワークの重み(パラメータ)を格納する多次元配列。モデルファイルの中身は、突き詰めるとこの「テンソルの集合」だ。

これらの用語が押さえられたら、safetensorsの背景を見ていこう。


3. safetensorsが生まれた背景

3.1 pickle地獄の実態

PyTorchのモデル保存は、歴史的に torch.save() / torch.load() を使っており、内部的にはpickleが使われていた。

# 従来の保存方法(内部的にpickleを使用)
torch.save(model.state_dict(), "model.bin")

# 読み込み(ここで任意コード実行のリスクがある)
state_dict = torch.load("model.bin")

2022年頃、実際にHuggingFace Hub上で悪意のあるpickleファイルが発見された事例が報告された。OSSモデルの配布が爆発的に増える中、「信頼できないソースからのモデル読み込み」はもはや日常的な行為だった。

3.2 HuggingFaceの回答:safetensors

HuggingFaceのNicolasことNarsil氏がRustで開発し、2023年に正式リリースされたのがsafetensorsだ。設計思想は明快で、「テンソルデータの保存にコード実行は一切不要」という原則を貫いている。

コアのRustライブラリはわずか約900行というコンパクトさ。それでいて、PyTorch、TensorFlow、JAX/Flax、NumPy、MLXと主要フレームワークすべてに対応している。

3.3 PyTorchも追随

PyTorch側も危機感を持ち、PyTorch 2.6で torch.load()weights_only パラメータのデフォルト値を変更した。さらに2025年6月には、PyTorch Distributed Checkpointing(DCP)がsafetensors形式をネイティブサポートすることを発表。もはやエコシステム全体がsafetensorsを前提に動き始めている。

背景がわかったところで、基本的な仕組みを見ていこう。


4. 基本概念と仕組み

4.1 ファイル構造:驚くほどシンプル

safetensorsのバイナリ構造は3つのパートだけで構成される。

┌─────────────────────────────────────┐
│  8 bytes: ヘッダーサイズ (uint64)      │  ← ヘッダーのバイト数
├─────────────────────────────────────┤
│  N bytes: ヘッダー (JSON UTF-8)       │  ← テンソルのメタデータ
├─────────────────────────────────────┤
│  残り: テンソルデータ (バイナリ)         │  ← 実際の重みデータ
└─────────────────────────────────────┘

ヘッダーはJSON形式で、各テンソルの名前・データ型・形状・データのオフセット位置が記述されている。

{
  "embedding": {
    "dtype": "F16",
    "shape": [32000, 4096],
    "data_offsets": [0, 262144000]
  },
  "attention.weight": {
    "dtype": "BF16",
    "shape": [4096, 4096],
    "data_offsets": [262144000, 295698432]
  }
}

ここに「実行可能なコード」が入り込む余地は一切ない。JSONのパースとバイナリデータの読み取りだけで完結する。これがsafetensorsの安全性の根拠だ。

4.2 pickle形式との比較

観点 pickle (.bin) safetensors (.safetensors)
安全性 任意コード実行のリスクあり コード実行不可。構造的に安全
読み込み速度 通常のデシリアライズ ゼロコピー対応で高速
遅延読み込み 不可(全テンソル一括読み込み) 可能(特定テンソルだけ読める)
ヘッダー形式 バイナリ(人間が読めない) JSON(人間が読める)
フレームワーク互換 PyTorch依存 PyTorch / TF / JAX / NumPy / MLX
ファイルサイズ やや大きい ほぼ同じ(メタデータ分だけ差)

4.3 遅延読み込み(Lazy Loading)の威力

safetensorsの隠れた武器が遅延読み込みだ。70Bパラメータのモデルを全部メモリに載せなくても、必要なテンソルだけを読み出せる。マルチGPU環境で各GPUに必要な層だけを配る場合に絶大な効果を発揮する。

4.4 最新動向:FP4/FP6サポート(2025年〜)

safetensorsの最新リリースでは、OCP(Open Compute Project)仕様に準拠したFP4/FP6データ型がサポートされた。バイトアラインメントされないビット幅のデータを扱えるようになり、次世代の量子化手法への対応が進んでいる。

基本概念が理解できたところで、実際にコードを書いて動かしてみよう。


5. 実践:実際に使ってみよう

5.1 環境構築

# safetensorsのインストール
pip install safetensors

# PyTorch連携も含める場合
pip install safetensors torch

5.2 環境別の設定ファイル

開発環境用(config.yaml)

# config.yaml - 開発環境用
project:
  name: "safetensors-demo"

environment: development

model:
  format: "safetensors"
  directory: "./models"
  framework: "pt"           # pt / tf / jax / numpy
  device: "cpu"             # 開発時はCPU

loading:
  lazy: true                # 遅延読み込みで省メモリ
  verify_hash: true         # 整合性チェック有効

logging:
  level: "DEBUG"

本番環境用(config.production.yaml)

# config.production.yaml - 本番環境用
project:
  name: "safetensors-demo"

environment: production

model:
  format: "safetensors"
  directory: "/opt/models"
  framework: "pt"
  device: "cuda"            # GPU推論

loading:
  lazy: false               # 本番では全読み込みで初回レイテンシ回避
  verify_hash: false        # 信頼済み環境ではスキップ

logging:
  level: "WARNING"

テスト環境用(config.test.yaml)

# config.test.yaml - CI/CD用
project:
  name: "safetensors-demo"

environment: test

model:
  format: "safetensors"
  directory: "./test_models"
  framework: "pt"
  device: "cpu"

loading:
  lazy: true
  verify_hash: true         # テストでは必ず検証

logging:
  level: "DEBUG"

5.3 基本的な使い方

"""
safetensors 基本操作デモ
実行方法: python safetensors_demo.py
必要パッケージ: safetensors, torch
"""
import torch
from safetensors.torch import save_file, load_file
from safetensors import safe_open


def demo_basic_save_load():
    """基本的な保存と読み込み"""
    # --- 保存 ---
    tensors = {
        "embedding": torch.randn(1000, 768),
        "attention.weight": torch.randn(768, 768),
        "attention.bias": torch.zeros(768),
    }
    save_file(tensors, "demo_model.safetensors")
    print("モデルを demo_model.safetensors に保存しました")

    # --- 一括読み込み ---
    loaded = load_file("demo_model.safetensors")
    print(f"読み込んだテンソル数: {len(loaded)}")
    for name, tensor in loaded.items():
        print(f"  {name}: shape={tensor.shape}, dtype={tensor.dtype}")


def demo_lazy_loading():
    """遅延読み込み(特定テンソルだけ読む)"""
    print("\n--- 遅延読み込みデモ ---")
    with safe_open("demo_model.safetensors", framework="pt", device="cpu") as f:
        # キー一覧を確認
        print(f"含まれるテンソル: {f.keys()}")

        # 必要なテンソルだけ読み込む
        embedding = f.get_tensor("embedding")
        print(f"embeddingのみ読み込み: shape={embedding.shape}")


def demo_slice_loading():
    """スライス読み込み(テンソルの一部だけ読む)"""
    print("\n--- スライス読み込みデモ ---")
    with safe_open("demo_model.safetensors", framework="pt", device="cpu") as f:
        # embeddingの最初の100行だけ読む(巨大モデルで有効)
        tensor_slice = f.get_slice("embedding")
        shape = tensor_slice.get_shape()
        print(f"embedding全体の形状: {shape}")

        partial = tensor_slice[:100, :]
        print(f"先頭100行だけ取得: shape={partial.shape}")


def demo_gpu_direct_load():
    """GPU直接読み込み"""
    if torch.cuda.is_available():
        print("\n--- GPU直接読み込みデモ ---")
        with safe_open("demo_model.safetensors", framework="pt", device="cuda:0") as f:
            tensor = f.get_tensor("embedding")
            print(f"GPU上のテンソル: device={tensor.device}")
    else:
        print("\n--- GPU非搭載のためスキップ ---")


if __name__ == "__main__":
    demo_basic_save_load()
    demo_lazy_loading()
    demo_slice_loading()
    demo_gpu_direct_load()

5.4 実行結果

$ python safetensors_demo.py
モデルを demo_model.safetensors に保存しました
読み込んだテンソル数: 3
  embedding: shape=torch.Size([1000, 768]), dtype=torch.float32
  attention.weight: shape=torch.Size([768, 768]), dtype=torch.float32
  attention.bias: shape=torch.Size([768]), dtype=torch.float32

--- 遅延読み込みデモ ---
含まれるテンソル: ['attention.bias', 'attention.weight', 'embedding']
embeddingのみ読み込み: shape=torch.Size([1000, 768])

--- スライス読み込みデモ ---
embedding全体の形状: [1000, 768]
先頭100行だけ取得: shape=torch.Size([100, 768])

--- GPU非搭載のためスキップ ---

5.5 よくあるエラーと対処法

エラー 原因 対処法
safetensors_rust.SafetensorError: Error while deserializing header ファイルが破損、またはsafetensors形式ではない ファイルの先頭8バイトを確認。他形式(pickle等)を誤って読んでいないかチェック
RuntimeError: Device not available 指定デバイスが利用不可 device="cpu" に変更して確認
KeyError: 'tensor_name' 指定したテンソル名が存在しない f.keys() で実際のキー一覧を確認
ImportError: No module named 'safetensors' 未インストール pip install safetensors
MisalignedByte エラー FP4/FP6でバイト境界不整合 テンソルの最終次元が2の倍数であることを確認

5.6 環境診断スクリプト

#!/usr/bin/env python3
"""
safetensors環境診断スクリプト
実行方法: python check_safetensors_env.py
"""
import sys


def check_environment():
    """safetensors環境をチェック"""
    issues = []
    info = []

    # Python バージョン
    info.append(f"Python: {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}")

    # safetensors
    try:
        import safetensors
        info.append(f"safetensors: {safetensors.__version__}")
    except ImportError:
        issues.append("safetensorsがインストールされていません: pip install safetensors")

    # PyTorch連携
    try:
        import torch
        info.append(f"PyTorch: {torch.__version__}")
        from safetensors.torch import save_file, load_file
        info.append("safetensors.torch: 利用可能")
    except ImportError as e:
        issues.append(f"PyTorch連携に問題: {e}")

    # TensorFlow連携
    try:
        import tensorflow as tf
        info.append(f"TensorFlow: {tf.__version__}")
    except ImportError:
        info.append("TensorFlow: 未インストール(任意)")

    # 結果
    print("=" * 50)
    print(" safetensors 環境診断レポート")
    print("=" * 50)
    print("\n[環境情報]")
    for item in info:
        print(f"  {item}")
    if issues:
        print(f"\n[問題点: {len(issues)}件]")
        for issue in issues:
            print(f"  - {issue}")
    else:
        print("\n  環境は正常です!")
    print("=" * 50)


if __name__ == "__main__":
    check_environment()

実装方法がわかったので、次は具体的なユースケースを見ていこう。


6. ユースケース別ガイド

6.1 ユースケース1: pickleモデルをsafetensorsに変換する

想定読者: 既存の .bin モデルを安全な形式に移行したい方

サンプルコード:

"""
pickle(.bin) → safetensors 変換スクリプト
実行方法: python convert_to_safetensors.py model.bin
"""
import sys
import torch
from safetensors.torch import save_file


def convert_pickle_to_safetensors(input_path: str, output_path: str = None):
    """pickleモデルをsafetensorsに変換"""
    if output_path is None:
        output_path = input_path.replace(".bin", ".safetensors")

    print(f"読み込み中: {input_path}")
    state_dict = torch.load(input_path, map_location="cpu", weights_only=True)

    # テンソル以外のデータがある場合はフィルタリング
    tensor_dict = {k: v for k, v in state_dict.items() if isinstance(v, torch.Tensor)}
    skipped = len(state_dict) - len(tensor_dict)
    if skipped > 0:
        print(f"  非テンソルデータ {skipped}件 をスキップ")

    save_file(tensor_dict, output_path)
    print(f"変換完了: {output_path}")
    print(f"  テンソル数: {len(tensor_dict)}")


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("使い方: python convert_to_safetensors.py <input.bin> [output.safetensors]")
        sys.exit(1)
    convert_pickle_to_safetensors(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else None)

6.2 ユースケース2: HuggingFaceモデルをsafetensorsで読み込む

想定読者: HuggingFace Transformersでモデルを使いたい方

サンプルコード:

"""
HuggingFace Transformersでsafetensorsモデルを読み込む
"""
from transformers import AutoModel, AutoTokenizer

# safetensors形式を明示的に指定
model = AutoModel.from_pretrained(
    "bert-base-uncased",
    use_safetensors=True  # safetensors形式を優先
)
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")

# 推論
inputs = tokenizer("Hello, safetensors!", return_tensors="pt")
outputs = model(**inputs)
print(f"出力形状: {outputs.last_hidden_state.shape}")

6.3 ユースケース3: マルチGPU環境での効率的な分割読み込み

想定読者: 大規模モデルを複数GPUに分散配置したい方

サンプルコード:

"""
safetensorsの遅延読み込みを使ったマルチGPU配置
"""
import torch
from safetensors import safe_open


def load_model_sharded(filepath: str, gpu_assignments: dict):
    """テンソルを指定GPUに直接読み込む"""
    loaded = {}
    with safe_open(filepath, framework="pt") as f:
        for tensor_name in f.keys():
            # このテンソルをどのGPUに配置するか決定
            device = gpu_assignments.get(tensor_name, "cpu")
            loaded[tensor_name] = f.get_tensor(tensor_name).to(device)
            print(f"  {tensor_name}{device}")
    return loaded


if __name__ == "__main__":
    # 例: 層ごとにGPUを割り当てる
    assignments = {
        "embedding": "cuda:0",
        "attention.weight": "cuda:0",
        "ffn.weight": "cuda:1",
    }
    # model = load_model_sharded("large_model.safetensors", assignments)
    print("GPU割り当て設定完了(実行にはsafetensorsモデルファイルが必要)")

ユースケースを把握できたところで、この先の学習パスを確認しよう。


7. 学習ロードマップ

初級者向け(まずはここから)

  1. safetensors公式ドキュメント - 基本APIの全体像を掴む
  2. HuggingFace - Load safetensors - Diffusers経由での使い方
  3. 手持ちの .bin モデルを本記事の変換スクリプトでsafetensors化してみる

中級者向け(実践に進む)

  1. safetensors GitHub でファイルフォーマット仕様を読む
  2. マルチGPU環境での遅延読み込みを自分のプロジェクトで試す
  3. PyTorch DCP safetensorsサポート で分散チェックポイントを学ぶ

上級者向け(さらに深く)

  1. Rustで書かれたコアライブラリ(約900行)のソースコードを読む
  2. カスタムデータ型(FP4/FP6)のサポート状況を追いかける
  3. 独自フレームワーク向けのバインディング開発にチャレンジ

8. まとめ

この記事では、safetensorsについて以下を解説した。

  1. セキュリティ問題の本質 - pickle形式の任意コード実行リスクと、safetensorsがそれをどう解決したか
  2. ファイル構造の仕組み - JSONヘッダー + バイナリデータというシンプルで安全な設計
  3. 実践的な使い方 - 保存・読み込み・変換・遅延読み込みまで

私の所感

safetensorsの凄さは「やらないこと」を明確に決めた設計にある。pickle形式が「何でもできる汎用性」を追求した結果セキュリティホールを生んだのに対し、safetensorsは「テンソルを安全に保存・復元する」という一点に集中した。

Rustで約900行という驚異的なコンパクトさは、この「引き算の設計」の賜物だ。2026年現在、PyTorch DCPのネイティブサポートも加わり、safetensorsはもはや「推奨」ではなく「前提」の時代に入っている。

HuggingFaceからモデルをダウンロードする際、.safetensors ファイルを見かけたら「ああ、安全なやつだな」と安心してほしい。そして、もし手元にまだ .bin ファイルがあるなら...今日が変換のベストタイミングだ。


参考文献

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?