0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

NVMe SSD 3枚のbtrfs RAID0を理論値と実測で検証した話

0
Last updated at Posted at 2026-10-21

手持ちの PC パーツを組み替えて開発用の Linux サーバーにしたので、NVMe SSD 3枚を btrfs の RAID0 にまとめて、I/O 性能がどう変わるかを検証しました。
主な目的は、ローカル LLM の大きなモデルファイル(50〜80GB)の読み込みを速くすることです。

3枚のうち1枚は、USB4 とレーンを分け合うソケットに挿していて x2 で接続されるので、RAID0 にしてもフル性能は出ないことは理屈の上で予想できていました。
それでも、どこまで理論どおりになるのか、x2 の1枚がどれだけ全体を引っ張るのか を数字で確かめたかったので、あえてその構成で組んで測り、後から x4 に切り替えて比べています。

進め方は、構成から理論値を見積もる → fio で実測する → 予想から外れたところの原因を掘る、の繰り返しです。
読み込みはほぼ理論どおりでしたが、書き込みと LLM のロードは理論値から大きく外れ、どちらも原因はストレージではなくソフトウェア側(btrfs と推論エンジンの実装)にありました。最後は推論エンジンに改善のプルリクエストを出しています。
(データは RAID0 なので、1枚壊れると多くのファイルが欠けます。メタデータは冗長化しているのでファイルの一覧は残り、欠けていない部分は救出できる可能性がありますが、全部失う覚悟のうえで、大事なデータはバックアップしつつ使う開発用の構成です)

結論

項目 予想 実測 原因・補足
RAID0 のシーケンシャル読み(x2 が1枚) 3.55 × 3 ≈ 10.6GB/s 10.6GB/s btrfs RAID0 は均等割りなので、一番遅い1枚に揃う
RAID0 のシーケンシャル読み(全部 x4) 6.2 × 3 ≈ 18.7GB/s 18.8GB/s 理論どおり(単体の3.0倍)
チップセット経由の影響 同時に使うと落ちるかも 落ちない 3枚同時でも各台が単体と同じ速度
RAID0 のシーケンシャル書き 3倍前後 ほぼ変わらず(2〜2.7GB/s) btrfs がチェックサム付きファイルへの O_DIRECT をバッファ書き込みに切り替えていた。普段の書き込みへの影響は小さい
LLM のモデルのロード(1スレッドの読み込み) 大幅に短縮 1割強 ページキャッシュ経由の読み込み処理(CPU 側)が約 6GB/s で頭打ち。推論エンジン(Strata)を直して起動が約4秒短縮

作業環境

  • マザーボード: ASRock X870 Taichi Creator(BIOS 4.60)
  • CPU / メモリ: AMD Ryzen 9 9950X3D / 96GB
  • SSD: Crucial T500 2TB(PCIe 4.0 x4)× 3枚
    • 3枚とも以前の PC から流用したもの(書き込み 28〜34TB、寿命消費 3%)。SSD 側が Gen4 までなので、Gen5 のソケットに挿しても Gen4 で動きます
  • OS: Ubuntu 26.04.1(カーネル 7.0.0-38)。ファイルシステムは btrfs(ssd,discard=async,space_cache=v2、圧縮なし)
  • 計測ツール: fio 3.41(io_uring、O_DIRECT)
  • 計測スクリプトと生ログはすべて GitHub に置いています: https://github.com/akivajp/nvme-raid-bench

構成と理論値の見積もり

3枚の SSD は、それぞれ経路の違うソケットに挿さっています。

ソケット 経路 リンク 見込み
M2_1 CPU 直結 Gen4 x4(ソケットは Gen5) 約 6.6GB/s
M2_2 CPU 直結 Gen4 x2(USB4 とレーンを分け合う) 約 3.5GB/s
チップセット側 チップセット経由 Gen4 x4(チップセットの上流 x4 を NIC・USB・SATA と共有) 単体なら約 6.6GB/s。同時に使うと上流で詰まるかも
  • 実際の接続は、sysfs で確認できます

    $ for d in /sys/class/nvme/nvme*; do
        echo "$(basename $d): x$(cat $d/device/current_link_width) (max x$(cat $d/device/max_link_width))"
      done
    nvme0: x4 (max x4)   # チップセット側
    nvme1: x4 (max x4)   # M2_1
    nvme2: x2 (max x4)   # M2_2: SSD 側は x4 に対応しているが x2 で接続
    
    • PCIe の上流のブリッジ(CPU のルートポート)まで辿ると、ルートポート自体の最大が x2 でした。残りの x2 は、隣の USB4 コントローラ(ASM4242)のルートポートに割り当てられています

      $ cat /sys/bus/pci/devices/0000:00:03.1/max_link_width   # nvme2 の上流のルートポート
      2
      
    • ちなみに、この BIOS 設定では USB4 を実際に使っていなくても x2 でした。レーンの分割は BIOS の設定で決まるので、OS からは変えられません。lspci で型番を眺めるだけでは気づきにくいので、上流まで max_link_width を見るのがおすすめです

  • RAID0 の読み込み速度の見込み

    • btrfs の RAID0 は、データを一定の大きさに区切って、各ドライブに 均等に 割り振ります
    • なので、全体の速度は「一番遅いドライブ × 台数」で決まるはずです。x2 の1枚がいる間は 3.55 × 3 ≈ 10.6GB/s が上限、全部 x4 なら 6.2 × 3 ≈ 18.7GB/s の見込みです
    • チップセット経由の1枚が上流で詰まるようなら、さらに下がります

測り方

  • Linux の定番ベンチマークツール fio を使い、条件は Windows でおなじみの CrystalDiskMark に寄せました
    • シーケンシャル 1MiB(キュー深さ 8 / 1、並列 4×32)、ランダム 4KiB(キュー深さ 32 / 1、並列 8×32)の読み書き
    • 各条件 3回の中央値。読み込みは 20秒、書き込みは 10秒
  • 実際の用途に近いものとして、キャッシュを捨てた状態から 54.8GB の GGUF(LLM のモデルファイル)を最後まで読む時間 と、LLM サーバーの起動時間 も測りました
  • 気をつけた点
    • 未書き込みの領域を読むと、異常に速い値が出ます。SSD は、一度も書いていない(TRIM された)領域を読まれると、NAND を読まずにゼロを返すためです。生デバイスは、計測範囲を先に書き埋めてから測りました
    • btrfs では、読み込みの計測を先にやります。コピーオンライト(CoW)なので、ランダム書き込みをするとファイルが細切れになり、その後の読み込みの結果が変わってしまいます
    • スロットリングの確認。M2_2 〜 M2_4 は大きめのヒートシンク付きのカバーがあるものの、真上に 3.5 スロット厚の RTX 4090 がいます。SMART の温度スロットリングのカウンタ(thm_temp1/2_trans_count)を計測ごとに前後で記録しました(結果: 全計測で 0回)

検証1: 単体性能と、チップセット経由の影響

まず RAID0 にする前に、1枚ずつ測りました(MB/s)。

M2_1(CPU 直結 x4)※ チップセット側(x4) M2_2(CPU 直結 x2)
SEQ1M Q8T1 読み 6,756 6,618 3,552
SEQ1M Q8T1 書き 2,012 6,239 3,360
RND4K Q32T1 読み 1,475 2,157 2,191

※ M2_1 はシステムディスクなので btrfs 上のファイルで計測。他の2枚は生デバイス

  • x2 の1枚は、見込みどおりシーケンシャルが約 3.5GB/s で頭打ちです。4K ランダムは他と同じで、x2 の影響はありません
  • チップセット経由の1枚も、単体なら CPU 直結とほぼ同じ速度でした
  • 次に、3枚を同時に読んでみると、合計 15.7GB/s(6.0 + 6.2 + 3.5GB/s)。3枚とも単体と同じ速度が出ているので、チップセットや CPU 側で帯域を取り合っている様子はありません(ほかのデバイスが暇なら、ですが)
  • M2_1 の書き込みだけが妙に遅い(2.0GB/s)のは、ドライブではなく btrfs 側の事情でした。検証3で扱います

検証2: RAID0 の読み込みは「一番遅い1枚 × 台数」

既存のシステムディスクに2枚を足して、データを RAID0 に変換します(メタデータは RAID1。後で RAID1C3 に変更しました。末尾の追記を参照)。

$ sudo btrfs device add /dev/nvme0n1 /dev/nvme2n1 /
$ sudo btrfs balance start -dconvert=raid0 -mconvert=raid1 /
単体 RAID0(x2 が1枚) RAID0(全部 x4)
SEQ1M Q32T4 読み 6,239 10,630(×1.70) 18,771(×3.01)
SEQ1M Q1T1 読み 4,002 6,581 9,863(×2.46)
RND4K Q32T1 読み 1,475 1,293 1,466(×0.99)
SEQ1M Q8T1 書き 2,012 1,907 2,667(×1.33)

(MB/s。全部 x4 の列は、BIOS で USB4 を無効にして M2_2 を x4 に切り替えた後の値)

  • x2 が1枚いる間は 10.6GB/s。3枚同時なら 15.7GB/s 出るのに、見積もりの 3.55 × 3 ≈ 10.6GB/s とぴったり同じ値で止まりました。速い2枚は、余力を残したまま x2 の1枚を待っていることになります
  • USB4 は今後もあまり使わない見込み(USB 3.2 Gen2 の Type-C が別にある)なので、BIOS で USB4 を無効にして M2_2 を x4 にすると、18.8GB/s。こちらも 6.2 × 3 ≈ 18.7GB/s の見積もりどおりです
  • ランダム 4K はほとんど変わりません。RAID0 が効くのは、まとまった量を読むときだけです
  • x4 にした M2_2 の最高温度は 66℃ → 74℃ に上がりましたが、スロットリングは起きていません
  • 読み込みはここまで理論どおり。一方で、書き込みは RAID0 にしてもほとんど伸びていません。生デバイス1枚でも 6.2GB/s 出るので、3枚なら 18GB/s 前後になるはずです

検証3: 書き込みが伸びない → btrfs が O_DIRECT をやめていた

  • btrfs 上の書き込みは、単体でも RAID0 でも 2〜2.7GB/s。台数を増やしても伸びないので、ストレージ以外のどこかで詰まっています

  • 調べると、2025年に入った btrfs の変更が原因でした

    • upstream の commit 968f19c5b1b7「btrfs: always fallback to buffered write if the inode requires checksum」(2025年3月。2026年2月には 6.6 / 6.12 の安定版にもバックポート)
    • btrfs はデータにチェックサムを付けます。ところが、O_DIRECT で書いている最中にアプリ側がバッファを書き換えると、ディスクに書かれた中身とチェックサムが食い違ってしまいます(VM のディスクイメージで実際に起きていたらしい)
    • そこで、チェックサム付きのファイルへの O_DIRECT 書き込みは、常にページキャッシュ経由の普通の書き込みに切り替える ことにした、という変更です。性能が落ちることは、コミットメッセージにも書かれています
    • fio の書き込みは O_DIRECT なので、まさにこれに当たっていたわけです
  • 本当に切り替わっているかは、/proc/vmstat の nr_dirtied(ページを Dirty にした回数の累計)で確かめられます。本物の O_DIRECT なら、ページキャッシュを通らないのでほぼ増えません

    $ d() { awk '$1=="nr_dirtied"{print $2}' /proc/vmstat; }
    $ a=$(d); fio --name=c --filename=testfile --rw=write --bs=1m --direct=1 --size=8g > /dev/null; b=$(d)
    $ echo "$(( (b-a)*4/1024 )) MiB"
    8200 MiB    # 8GiB 書いて、全部ページキャッシュを通っている
    
    • チェックサムを付けない設定(chattr +C)のファイルで同じことをすると 0 MiB でした
    • ちなみに、free で見える Cached の増え方では判別できません。切り替わった場合も、O_DIRECT の約束を守るために、書き終わったページをカーネルがすぐに捨ててしまうためです
  • では、チェックサムを外すとどれくらい変わるのか。RAID0(全部 x4)の上で比べました(MB/s)

    通常のファイル chattr +C のファイル
    O_DIRECT 書き込み(Q8T1) 2,632 12,584(×4.8)
    普通の書き込み(32GiB + fsync、1ファイル) 3,867 4,071(×1.05)
    普通の書き込み(4ファイル並列) 7,428 8,780(×1.18)
    • O_DIRECT なら約5倍。でも、普通の書き込みではほとんど差がありません
    • コピーやダウンロードのような普段の書き込みは、もともとページキャッシュ経由です。なので「btrfs は書き込みが遅い」のではなく、ベンチマーク(O_DIRECT)の数字が低く出ているだけ と言えます
    • 1ファイルの書き込みは、書き手1本の CPU が 100% で頭打ちでした。複数のファイルに分けて並列に書けば伸びます
  • 対処

    • 基本は何もしない。chattr +C にすると、チェックサムによる破損検出・圧縮・reflink が使えなくなるので、全体に付けるのは割に合いません
    • DB や VM イメージのように O_DIRECT で書くものの置き場所だけ、空のディレクトリに chattr +C を付ける(既にデータがあるファイルには後から付けられません)
    • ちなみに、チェックサムを保ったまま O_DIRECT を速くする修正(IOMAP_DIO_BOUNCE。本来の速度の約95%とのこと)も提案されていて、2026年6月の時点では btrfs 側が未マージでした。新しいカーネルでは解決しているかもしれません

検証4: LLM のモデルのロードは、ページキャッシュの読み込みで頭打ち

本命の用途です。キャッシュを捨てた状態から、54.8GB の GGUF を最後まで読む時間を測りました。

読み方 単体 RAID0(全部 x4)
O_DIRECT で並列に読む 12.5 秒 4.3 秒(×2.9)
read() で1スレッドで読む 10.6 秒 8.8 秒(×1.2)
mmap で1スレッドで読む 11.1 秒 9.9 秒(×1.1)
  • O_DIRECT で並列に読めば約3倍。一方、普通に1スレッドで読むと 1割ちょっとしか縮みません

  • 1スレッドの読み込みは 6GB/s 前後で頭打ちで、CPU 使用率は 80〜90%。ストレージではなく、ページキャッシュからユーザー空間へデータを写す処理(CPU 側) が律速です

  • 実際の LLM サーバーでも、起動してから最初の応答が返るまでを測りました(Qwen3.8-Flash-Next IQ3_S、GGUF 83.6GB)

    キャッシュなし キャッシュあり
    Strata(MoE 向けの推論エンジン。普段使い) 15.5〜16.0 秒 16.5 秒
    llama.cpp(mmap、既定) 17.9 秒 10.1 秒
    llama.cpp(--load-mode dio) 15.0 秒 14.4 秒
    • llama.cpp は、キャッシュがない状態なら --load-mode dio(O_DIRECT)が一番速くなりました。モデルがページキャッシュに残っているなら、既定の mmap の方が速いです
    • Strata は、ロード中の読み込みが約 6GB/s で一定。キャッシュの有無でも、読み込みのスレッド数を増やしても変わりません(Strata はエキスパートを RAM に固定するので、96GB だとモデルのキャッシュがそもそも残らない、という事情もあります)

Strata のローダーを読んで直す

  • Strata はオープンソースなので、ソースを読んでみました

    • Linux では、GGUF からのエキスパートの読み込みが fread で一時バッファに読み、memcpy で写す 方式でした。つまりページキャッシュ経由です
    • Windows には O_DIRECT 相当(FILE_FLAG_NO_BUFFERING)で読む経路が既にあって、Linux 側だけ (void) unbuffered; と未実装でした
  • 読み込み部分だけを C で再現して、読み方を比べました(32GiB、6スレッド、キャッシュなし)

    読み方 GiB/s
    fread + memcpy(Strata の現行方式) 5.88
    O_DIRECT + memcpy(Windows の経路を Linux に移した形) 11.80
    O_DIRECT で読み先へ直接 14.42
    • 現行方式の 5.88GiB/s は、Strata 本体で観測した値とぴったり一致します。スレッドを 16本にしても 5.90GiB/s で変わりません
  • そこで、Windows の経路を Linux に移植するパッチを書いて、Strata 本体で試しました

    • 変更は src/core/expert_source.cpp への 104 行の追加だけです。既存の環境変数 STRATA_UNBUFFERED_LOAD=1 を付けたときだけ O_DIRECT で読みます(これまで Linux では、この変数を付けても読み込みのスレッドが増えるだけでした)
    • O_DIRECT で開けないファイルシステムでは、従来の読み方に戻ります
    Strata 0.2.2(同じイメージで交互に3回ずつ) エキスパートの読み込み 起動から応答可能まで
    既定 5.65〜5.72 GiB/s 17.9〜19.4 秒
    パッチ + STRATA_UNBUFFERED_LOAD=1 11.97〜12.04 GiB/s 13.9〜14.0 秒
    • 読み込み部分は約2倍、起動全体では約4秒(2割強)短くなりました(普段使いの 0.1.45 でも同じパッチで 16.0 → 11.5 秒)。残りの約10秒は、CUDA の初期化や GPU への転送など、ストレージと関係のない部分です
    • 読み込んだデータのチェックサム(Strata の STRATA_VERIFY_ARENA=1)が、既定の読み方と完全に一致することも確認しました
  • このパッチは、プルリクエストとして Strata に送っています(AMD 向けのビルドを確認できていないので、ドラフトの状態です): https://github.com/Niko1221/Strata/pull/3624

補足: fio の mmap は並列にすると実際より速く出る

  • llama.cpp の mmap を真似て、fio の --ioengine=mmap --numjobs=8 で GGUF を読んだら 28GB/s と出ました

    • 3枚同時に読んでも 18.8GB/s が上限なので、物理的にありえない数字です
  • /proc/diskstats で、実際にディスクから読んだ量と突き合わせると

    fio の報告 実際にディスクから読んだ量
    mmap × 8並列 54.8 GB 6.9 GB
    mmap × 1 54.8 GB 54.9 GB
    psync × 8並列 54.8 GB 55.0 GB
    • mmap エンジンで並列にすると、各ジョブの担当範囲(offset_increment)がずれず、全員が同じ範囲を読んでいた ようです(2人目以降はページキャッシュから読むので速い)
    • エラーも警告も出ないので、物理的な上限と照らし合わせないと気づけません。mmap は1ジョブで測り、並列の読み込みは psync で測るようにしました

まとめ

  • 理論どおりだったこと
    • btrfs RAID0 の読み込みは「一番遅いドライブ × 台数」。x2 の1枚がいると 10.6GB/s、全部 x4 で 18.8GB/s(単体の3倍)
    • チップセット経由のソケットも、同時に使って帯域が詰まることはなかった
  • 理論から外れたこと(原因はソフトウェア側)
    • 書き込み: btrfs が、チェックサム付きのファイルへの O_DIRECT をバッファ書き込みに切り替えていた。ベンチマークの数字が低く出るだけで、普段の書き込みへの影響は小さい
    • LLM のロード: 1スレッドでページキャッシュ経由で読む処理が約 6GB/s で頭打ち。ローダーが O_DIRECT で読めば RAID0 が活きる(Strata にパッチを送付)
  • ストレージを速くしても、その手前のソフトウェアが同じペースで受け取れないと効果が出ない、というのが一番の学びでした。とはいえ容量は 5.5TB になり、モデルを置き放題になったのは素直に嬉しいです

お疲れ様でした!

追記(2026/10/22): メタデータを RAID1C3 にした

  • 記事の中では、メタデータを RAID1(2枚に同じものを書く)にしていました。ディスクが3枚あるので、3枚すべてに書く RAID1C3 に変更しました

    • データは RAID0 のままなので、1枚壊れれば多くのファイルは欠けます。一方で、メタデータ(ファイルの一覧や配置の情報)は、RAID1 なら残りの1つ、RAID1C3 なら残りの2つから読めます。救出作業の途中で残りのディスクに読み取りエラーが出ても、もう1つの複製から直せるぶん、RAID1C3 のほうが安心です

      $ sudo btrfs balance start -mconvert=raid1c3 /   # -m を指定すると、System も同じ構成に変換される
      $ btrfs filesystem df /
      Data, RAID0: total=708.00GiB, used=681.75GiB
      System, RAID1C3: total=32.00MiB, used=80.00KiB
      Metadata, RAID1C3: total=8.00GiB, used=5.56GiB
      GlobalReserve, single: total=512.00MiB, used=0.00B
      
      • 変換するのはメタデータ(約 5.5GiB)だけなので、すぐに終わりました
      • RAID1C3 は Linux 5.5 で入った機能で、変換後は 5.5 より古いカーネルではマウントできなくなります(古いレスキュー用 USB などで起動するときは注意)
  • メタデータを3か所に書くようになるので、書き込みが遅くならないかを変換の前後で測りました(3回の中央値)

    RAID1 RAID1C3
    空ファイル 20万個の作成(sync まで) 19.05 秒 17.51 秒
    RND4K Q32T1 書き込み 189 MB/s 199 MB/s
    RND4K Q32T8 書き込み 500 MB/s 542 MB/s
    SEQ1M Q8T1 書き込み 2,289 MB/s 2,598 MB/s
    SEQ1M Q32T4 書き込み 3,436 MB/s 3,329 MB/s
    • どれも回ごとのばらつき(btrfs のランダム書き込みは ±2割ほど揺れます)の範囲で、遅くなった様子はありません。容量はメタデータの1つ分(約 5.6GiB)余計に使いますが、3枚で 5.5TB あるので誤差ですね

この記事は個人ブログ(KANAF Works)にも掲載しています。

0
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?