FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

FPGA開発日記 カテゴリ別インデックス

続きを読む

RISC-VのIOMMUの仕組み (3. IOMMUが扱う2段階のアドレス変換)

IOMMUが取り扱うべきアドレスについてまとめよう。CPUには通常仮想アドレス(Virtual Address: VA)物理アドレス(Physical Address: PA)が定義されている。

しかし、ハイパーバイザが入ってくると話は別になってくる。ハイパーバイザは、ユーザソフトウェアとシステムソフトウェア(オペレーティングシステム)に加えて、ハイパーバイザが入ってくる。 つまり、OSが使っているアドレス空間は本当の物理アドレスではなく、ハイパーバイザによりさらに仮想化されている、というケースを考えるわけだ。 これにより、ハイパーバイザが導入されたRISC-V CPUでは、2段階のアドレス変換を考慮する必要がある。つまり:

  • 第1段階: 仮想アドレス (VA) → ゲスト物理アドレス (GPA)
  • 第2段階: ゲスト物理アドレス (GPA) → スーパバイザ物理アドレス (SPA)

IOMMUでは、これらのアドレス変換を独立に制御することができる。システムの構成に寄って、第1段階・第2段階の変換を行うかどうかを制御できる。

第1段階 第2段階 説明
Bare Bare 変換は行わない。状況としてはCPUも直接物理アドレスを使っている状態であり、Bare Metalソフトウェアが動いているような状況と思われる。
Bare !Bare GPA → SPA のみの変換を行う。状況としてはハイパーバイザ上で動いているOSが仮想アドレスを使用しない状態で使われる。
!Bare Bare VA → GPAのみの変換を行う。ハイパーバイザが存在しないか、ハイパーバイザ上で動作するOSが直接IOMMUをコントロールする場合がこれに相当する
!Bare !Bare VA → GPA → SPAの2段階の変換を行う。仮想マシン上のソフトウェアがIOMMUを通じてデバイスを制御する場合がこれに相当する。

RISC-VのIOMMUの仕組み (2. IOMMUが使われる流れ)

次に、IOMMUが実際のシステム内でどのように使われるのか、仕様書に含まれる図を眺めながら見てみよう。

下記の図には、大きく分けて以下の重要なコンポーネントが含まれている。

  • RISC-V Cores: RISC-V CPU自体である。
  • IOデバイス: IO Device AとIO Device Bが相当する。AIアクセラレータなどの、ハードウェアデバイスだと考えればよい。
  • PCIe Rootポート: PCIeデバイスのためのルートポート
  • IOMMU0 / IOMMU1: IOMMU本体である
  • IO Bridge: IOMMUに接続されており、IOデバイスやルート・ポートからのIOVAリクエストをIOMMUを介してPAに変換する。

上記で説明した通り、IO DeviceやPCIe ルート・ポートが生成するIOVAを、IO BridgeおよびIOMMUを介してSystem Interconnectに接続されている。

下側のIOMMU1およびIO Bridge1については、ルート・ポートを使用せずにIO Device AとIO Device BをSystem Interconnectに接続している。

IOMMUのインタフェース

IOMMUには、いくつかのインタフェースが接続されている。

  • Host Interface: グローバル・コンフィギュレーションやメンテナンスを行うためのインタフェース。System Interconnectからメモリ・マップされており、RISC-V Coresなどからコンフィグレーションを行うためのインタフェースである。
  • Data Translation Request: IOデバイスなどからのIOVAのアドレスを、IO Bridgeを介して変換する要求を受け取るためのインタフェース
  • Data Translation Complete: 上記のリクエストの変換完了を通知するためのインタフェース
  • Data Structure Interface: IO Bridgeに渡っているが、実際にはメインメモリにアクセスを行うためのインタフェースであり、アドレス変換に必要な情報をメインメモリから取得するために使用する。
  • ATS Interface: オプションのPCIeATS機能がIOMMUでサポートされている場合に使用される。ATSはAddress Translation Serviceの略であり、PCIe自体が持っているアドレス変換の仕組みである。

RISC-VのIOMMUの仕組み (1. 概要)

Chapter 1: IOMMU(入力出力メモリ管理ユニット)とは?

IOMMUとは、その名の通りIOデバイス向けのMMUである。IOデバイスというのは、GPGPU、AI Acceleratorなどの”CPU”以外のデバイスを指す。

MMUというのは、Memory Management Unitで、基本的な機能としては仮想アドレス(VA)から物理アドレス(PA)への変換や、メモリ領域の保護をサポートするものである。

IOMMUはCPUの使うMMUと基本的な役割は同じで、デバイス向けに、デバイスの使う仮想アドレスを物理アドレスに変換する役割と、デバイスが不正にメモリアクセスを行うことを防ぐものである。

IOMMUのユースケース

非常に簡単なモデルで、IOMMUがどのように使われるのかを考えてみる。例えば、CPUで動いているあるプロセスが、GPGPUに仕事を頼みたいとする。

CPUはGPGPUに対して、「仮想アドレス(VA) = 0x100にあるデータを処理してください」と頼む。ここで、VA=0x100は実際には 0x8000_0100 のことであるので、GPGPUは 0x8000_0100にアクセスする必要があるわけだ。

ところが、CPUのプロセスは基本的にソフトウェアからはアドレスはVAしか見えないので、CPUのプロセスが物理アドレス(PA)を知ることはできない。

したがって、CPUがGPGPUに伝えるときは「0x100」というアドレス情報しか伝えられなくて、GPGPUはこのVAが実際にはどの物理アドレスなのかを知る手段がない、ということになってしまう。

そこでIOMMUが登場する。IOMMUはGPGPUデバイス向けに0x100が0x8000_0100に相当するというような変換テーブルを保持しており、GPGPUからの要求に応じて物理アドレスを提供する。

この変換テーブルを生成するためには、CPU(そして具体的にはオペレーティングシステムに相当するシステムソフトウェア)がIOMMUのテーブルを設定し、GPGPUが適切にVA→PA変換ができるようにサポートする。

IOMMUがデバイスを管理する仕組み「デバイス・コンテキスト」

IOMMUがデバイスを管理するための概念として、デバイス・コンテキストというものを導入する。

デバイス・コンテキストというのは、IOMMUが各デバイスを管理するための情報を一元化したもので、IOMMUはデバイス毎にデバイス・コンテキストを持っている。

下記の図でいうならば、GPGPUはプロセスAで使われるので、プロセスAが持っているVA→PA変換に従い、プロセスBがAIアクセラレータを使うとしたら、プロセスBのVA→PA変換に従う必要がある。これらのデバイス毎の情報をまとめたものが、デバイス・コンテキストである。

冬休み読書日記2024: 並列コンピュータ ー非定量的アプローチ

結構前に勝って積読していた本を読み直そうとしている。

前書きの所に書いてあるが、この本は「非定量的アプローチ」というところで具体的な数値やグラフなどを一切排除している。 その結果、技術書というよりも読み物として軽く読める感じに仕上げている。

最初の所は、基本的な並列コンピュータの構成方法とキャッシュのコヒーレンシのとり方。スヌーププロトコルの構成などについて。

キャッシュコヒーレンスを管理するバスプロトコルの勉強 (4. それぞれのプロトコルでのキャッシュコヒーレンシのとり方3)

例えば、4CPU + L2キャッシュが存在している状態をまずは考えよう。前回の記事の状態から、CPU2が書き込みを発生させるとどうなるか。

※以下の説明はChatGPTにかなり頼っているところがある。間違っているところもあるかもしれない...

ACEプロトコル

ACE にはいくつかのやり方があるが、一般的なライトアロケート(Write-allocate) ポリシーのシナリオを考える。

CPU2 がストアを実行すると、内部的には ReadForOwnershipReadUnique 相当のリード要求をまず発行して、ラインを“自コア排他”の状態で取得してから書き込みを行う。

あるいは、AWチャネルWriteUnique を直接発行する形もある。

  1. CPU2 は ACEのマスタIFから以下のような要求を出す
    • 例: ARチャネルで ReadUnique(自分が排他権を取りたい)
    • または AW Channel.awunique=1 で WriteUnique を開始する実装もあり。
  2. L2(インターコネクト) は、「現在 CPU0, CPU1 が Shared で持っている」ことをタグやディレクトリから把握
    • そこで、AC Channelを使い、CPU0 と CPU1 に対して Invalidate (または CleanInvalid) などのスヌープ要求を送る
    • CPU0, CPU1 は “Shared” で保持していただけ(Dirtyではない)ため、「キャッシュラインを無効化しました」という応答(CR Channel)を返すだけでデータ書き戻しは不要
  3. CPU2Exclusive / Modified 状態を獲得
    • L2 は書き込みデータを CPU2 に渡す、もしくは CPU2 が自分のキャッシュにラインを取り込んで書き込み
    • このとき L2 も内部的に “Owned/Dirty” としてマークするか、あるいは「CPU2 が Modified で保持している」記録を保持
  4. CPU0, CPU1 は 該当ラインを Invalid に遷移

CHIプロトコル

CHIでは、CPU2 (Requester) が「Write」を行いたい場合、通常はExclusive or Owned状態を取得するためのリクエストを REQチャネル で発行する。

代表的には “ReadForOwnership”“WriteNoSnoop”(ただし後者は本来Non-coherentアクセスの場合)など、「排他権を得たい」という意図を示すコマンドが送られます。

  1. CPU2L2: REQチャネルで “ReadForOwnership” (または類似コマンド) を発行する
  2. L2 (Home Node) は自分のディレクトリを確認し、「CPU0, CPU1 が Shared 保持中」と判定
    • SNP Channelを使い、CPU0, CPU1 に “Invalidate” や “MakeInvalid” 等のスヌープ要求を送る
    • CPU0, CPU1 とも “Shared (Clean)” なので書き戻し不要の応答を返しつつ、Invalid に遷移
  3. スヌープ完了後、L2 は CPU2 に対し、DAT Channelでラインを渡し、RSP Channel で「Exclusive/Modified権限を与えた」という応答を返す
  4. CPU2 は最終的にそのラインを Modified (または Exclusive) 状態で取得し、書き込みが可能となる

TileLink では、書き込み(ストア)を行う前に “Acquire” で Exclusive/Dirty の権限を取得する必要がある。

これにより、他のクライアントが Shared で持っているラインを無効化させます。

  1. CPU2L2: Aチャネルで “Acquire (Grow)” を送信
    • Grow”パラメータとして「ブロックを Exclusive あるいは Dirty にしたい」(TileLink用語では “to Trunk/Tip” のような指定)
  2. L2(Manager) は「他コアが Shared を保持している(CPU0, CPU1)」のを把握
    • B Channel(Probe) を CPU0, CPU1 に送って「そのラインをダウングレード/無効化しろ」と要求
    • CPU0, CPU1 は C Channel(Release) で「Cleanとなった」と応答しつつキャッシュラインを Invalid に遷移
    • L2 は書き戻し不要(クリーン)だと判断
  3. プローブ完了後、L2D Channel(Grant) で CPU2 に「Exclusive/Dirty権限付与」を通知し、必要なデータを転送
  4. CPU2 は Eチャネル(Finish) で完了を応答し、キャッシュラインを トランク(Trunk)/チップ(Tip) と呼ばれる「排他・書き込み可能」状態にセットし、ストア命令を実行できる。

冬休みプログラミングレッスン2024: Advent of Code (Day06)

adventofcode.com

久しぶりにC++を使ってプログラミングしている。なんかソフトウェアが書けなくなって参ってしまうな...

6日目。マップを通りながら、石にぶつかると方向を変えていく。何ステップで脱出できるかという話だが、素直にマップ上をシミュレーションすると回答できる。

問題はPart-Twoで、おそらく現在の場所から、次の場所に石を置いた場合にループが発生するかを見ていけばよいはずなのだが、まだうまくいっていない。 ループの発生は、石にぶつかったときにすでにその場所が訪問済みならば、同じ方向に曲がるはずなのでそれをループとすればよいと思う。

#include <iostream>
#include <vector>

bool out_of_map (std::pair<int, int> v, size_t max) {
    if (v.first < 0 || v.first >= max || v.second < 0 || v.second >= max) {
        return true;
    } else {
        return false;
    }
}


void show_map (std::vector<std::string> map) {
    std::cout << '\n';
    for (auto v: map) {
        std::cout << v << '\n';
    }
}

int main ()
{
    std::string line;
    std::vector <std::string> v_map;
    std::pair <int, int> v_point;
    size_t start_y = 0;

    typedef enum {
        UP = 0, 
        RIGHT = 1   ,
        DOWN = 2,
        LEFT = 3
    } dir_t;

    while (std::cin >> line) {
        v_map.push_back(line);
        for (size_t i = 0; i < line.size(); i++) {
            if (line[i] == '^') {
                v_point.first  = start_y;
                v_point.second = i;
            }
        }
        start_y++;
    }
    size_t map_size = v_map.size();
    dir_t v_dir = UP;
    size_t v_step = 0;
    bool map_finish = false;

    while (!map_finish) {
        if (v_map[v_point.first][v_point.second] != 'X') {
            v_step++;
        }
        v_map[v_point.first][v_point.second] = 'X';
        switch (v_dir) {
            case UP: {
                v_point.first--;
                if (out_of_map (v_point, map_size)) {
                    map_finish = true;
                    break;
                }
                if (v_map[v_point.first][v_point.second] == '#') {
                    v_point.first++; // Role back
                    v_dir = RIGHT;
                    show_map (v_map);
                }
                break;
            }
            case RIGHT: {
                v_point.second++;
                if (out_of_map (v_point, map_size)) {
                    map_finish = true;
                    break;
                }
                if (v_map[v_point.first][v_point.second] == '#') {
                    v_point.second--; // Role back
                    v_dir = DOWN;
                    show_map (v_map);
                }
                break;
            }
            case DOWN: {
                v_point.first++;
                if (out_of_map (v_point, map_size)) {
                    map_finish = true;
                    break;
                }
                if (v_map[v_point.first][v_point.second] == '#') {
                    v_point.first--; // Role back
                    v_dir = LEFT;
                    show_map (v_map);
                }
                break;
            }
            case LEFT: {
                v_point.second--;
                if (out_of_map (v_point, map_size)) {
                    map_finish = true;
                    break;
                }
                if (v_map[v_point.first][v_point.second] == '#') {
                    v_point.second++; // Role back
                    v_dir = UP;
                    show_map (v_map);
                }
                break;
            }
        }
    }

    std::cout << v_step << '\n';

    return 0;
}