入出力とバス — 割り込み、DMA、そしてPCIe
一言でいうと
デバイスとやり取りする方法は、3つの層に整理されます。いつ知らせるか(ポーリング対割り込み)、誰が運ぶか(CPU対DMA)、どこを通るか(バスとインターコネクト)です。
なぜ必要なのか
CPUはナノ秒単位で動き、ディスクやネットワークカードはマイクロ秒からミリ秒単位で動きます。この速度差をそのままにすると、CPUはデバイスを待つことに時間をすべて使ってしまいます。入出力設計の歴史は、この差をどう隠すかに対する答えの積み重ねです。
どう動くのか
ポーリングと割り込み: ポーリングは、CPUがデバイスのステータスレジスターを読み続ける方式です。単純でレイテンシは短いですが、その間、CPUは何もできません。割り込みは、デバイスの準備ができたらCPUに信号を送る方式です。CPUはその間に別の仕事をしますが、割り込みのたびに、コンテキストを保存してハンドラーへ飛ぶコストがかかります。そのため、毎秒数十万パケットが届くネットワークカードでは、割り込みがかえってシステムを麻痺させることがあり(割り込みストーム)、LinuxのNAPIのように、最初は割り込み、忙しいときはポーリングへ切り替える混合方式が標準になりました。
DMA: データをCPUレジスターを経由して1ワードずつ移すと、CPUがデータの運搬に縛られます。DMAコントローラーは、デバイスとメモリの間で直接データを移し、すべて終わったら割り込みを1回だけ上げます。現代のシステムでは、大容量の転送は例外なくDMAです。
MMIOとポートI/O: デバイスのレジスターをメモリのアドレス空間に載せておくと、通常のロード/ストアでアクセスできます(MMIO)。構文がメモリと同じだという点が落とし穴です。コストはまったく違います。 MMIOの読み取りは、応答を待たなければならないノンポステッドトランザクションなので、PCIeの往復時間がそのまま実行時間になり、キャッシュに載せることもできません。ドライバーのコードがループの中でデバイスのレジスターを読んでいるなら、そのループは計算ではなく、I/O往復の繰り返しです。
PCIe: 帯域幅は、レーン数と世代で決まります。世代が上がるたびに、レーンあたりの速度がおおよそ2倍になり、x1/x4/x8/x16のようにレーンを束ねて幅を広げます。GPUをx16スロットではなくx4スロットに挿すと、演算性能はそのままで、データの供給が4分の1になります。複数のGPUを束ねて学習するときに、NVLinkのような専用のインターコネクトを使う理由も同じです。PCIe経由のカード間通信がボトルネックになるからです。
現場での姿
ホームラボでGPUノードを構成するときに、最もよくある間違いが、スロットの帯域幅です。マザーボードの2番目、3番目のスロットは、電気的にx4であることが多いので、カードを3枚挿すと、2枚が静かに4分の1の帯域幅で動作します。学習自体はできますが、ステップ時間が説明のつかないほど長くなります。lspci -vvで、ネゴシエートされたリンク幅を確認する習慣が、時間を節約してくれます。
ボトルネックがどこかを数字で見る
帯域幅は、桁で覚えると感覚がつかめます。
| 経路 | 帯域幅(おおよそ) |
|---|---|
| CPU ↔ L1キャッシュ | 1 TB/s以上 |
| CPU ↔ メモリ(DDR5) | 50–100 GB/s |
| PCIe 4.0 ×16 | 32 GB/s |
| PCIe 4.0 ×4(NVMe 1枚) | 8 GB/s |
| 10 GbEネットワーク | 1.25 GB/s |
| SATA III | 0.6 GB/s |
GPUを複数枚使うとき、PCIeがボトルネックになります。 32GB/sはメモリ帯域幅の半分にもならないので、カード間でデータを頻繁に移す作業(モデル並列)は遅くなります。NVLinkがある理由がこれです。
レーン数も見ます。×16スロットに挿しても、マザーボードが×4でしか接続していなければ、帯域幅が4分の1です。
lspci -vv -s <슬롯> | grep -E 'LnkCap|LnkSta'
LnkCap: Speed 16GT/s, Width x16 ← 장치가 할 수 있는 것
LnkSta: Speed 16GT/s, Width x8 ← 실제로 연결된 것 ← 절반이다
この2行が違えば、スロットや分岐(bifurcation)設定の問題です。GPU性能が期待の半分なら、ここから見ます。
割り込みとポーリング
デバイスが仕事を終えたときにCPUに知らせる方式は、2つあります。
- 割り込み: デバイスがCPUを呼びます。待ち時間が短いですが、コンテキスト切り替えのコストがあり、毎秒数十万件になると、そのコストが大きくなります。
- ポーリング: CPUが確認し続けます。スループットは高いですが、待機しているときもCPUを使います。
高速ネットワークとNVMeは、この2つを混ぜます。 LinuxのNAPIは、パケットが集中すると、割り込みを切ってポーリングへ切り替えます。そのため、si(softirq)の使用率が高いのは、ネットワークが忙しいという意味です。
ethtool -c eth0の割り込み統合(coalescing)の設定が、このバランスを調整します。値を大きくするとスループットが上がり、レイテンシが増えます。
DMAが行うこと
デバイスがメモリに、CPUを経由せず直接読み書きします。これがなければ、ディスクから1GBを読むときに、CPUがその1GBをすべて運びます。
ここで出てくるのがゼロコピーです。ファイルをソケットへ送るとき、通常はディスク → カーネルバッファー → ユーザーバッファー → カーネルのソケットバッファー → NICと、4回コピーしますが、sendfile()やsplice()は、ユーザー空間を飛ばします。静的ファイルサーバーが速い理由で、nginxのsendfile onがそれです。
続くクイズで確認すること
同じmov命令が、なぜあるアドレスではナノ秒、あるアドレスではミリ秒なのかを、説明できるかを確認します。