タグ: RISC-Vベクトル拡張

  • RISC-Vベクトル拡張、処理する要素数をCPUに合わせる仕組み

    RISC-Vベクトル拡張、処理する要素数をCPUに合わせる仕組み

    配列に並んだ数値を足す、画像の画素を加工する。こうした処理では、一つの命令で複数の値を扱うベクトル演算が役立ちます。ただ、CPUによって一度に扱えるデータの幅が違うと、プログラムは何個ずつ進めればよいのでしょうか。

    RISC-Vのベクトル拡張(RVV)は、処理したい個数を伝え、今回扱う個数をCPUから受け取る仕組みを備えています。レジスタの長さを表すVLENと、今回の処理数を表すvlを分けると、異なる幅のCPUで同じループを動かせる理由が見えてきます。ここでは、公式マニュアル20260120版に収録されたV拡張Version 1.0をもとに整理します。[1]

    簡単にまとめると?

    • VLENはレジスタ1本のビット数、SEWは要素1個のビット数、LMULはレジスタをどうまとめて使うかの設定です。処理できる最大の要素数は、この組み合わせで決まります。
    • ループはCPUが返したvl個だけ処理し、その分だけ配列の位置と残り個数を進めます。幅の違いや最後の端数を、同じ仕組みで扱えます。
    • 大きなレジスタや大きなLMULが、そのまま高速化の倍率になるわけではありません。対応命令、実行器、メモリ供給も確かめる必要があります。

    VLEN・SEW・LMULは、何の大きさなのか

    まず、レジスタはCPUが計算に使う値を置く場所です。RVVには32本のベクトルレジスタがあり、1本の長さをビット数で表したものがVLENです。これはCPUの実装で決まり、vsetvli命令で伸び縮みするものではありません。[1]

    その入れ物をどの幅の値に区切るかが、SEW(Selected Element Width、選択する要素幅)です。たとえば128ビットのレジスタを32ビットの整数に区切れば4個、64ビットなら2個が入ります。同じ入れ物でも、要素の大きさが変われば個数は変わります。

    LMUL(ベクトル長乗数)は、1回の命令で使うレジスタのまとまり方を決めます。LMUL=2なら、通常の同じ幅の演算では2本を一組として扱います。整数の設定は1、2、4、8で、対応条件のもとでは1/2などの分数設定も使えます。[1]

    この組み合わせから求まる最大要素数がVLMAXです。式はVLMAX = LMUL × VLEN ÷ SEW。ビット数ではなく、何個の要素を扱えるかを表します。

    要素幅とレジスタのまとめ方で変わる最大要素数(仕様の式から作成)
    VLEN SEW LMUL VLMAX 読み方
    128ビット 32ビット 1 4個 1本に32ビットの値が4個
    256ビット 32ビット 1 8個 1本が長くなり、8個入る
    128ビット 32ビット 2 8個 2本をまとめ、8個入る
    128ビット 64ビット 1 2個 要素が大きくなり、2個入る

    表の値は、各設定に対応する実装を想定した計算例です。どれも実機の速度測定ではありません。また、256ビットの1本と128ビットの2本は、容量が同じでも、内部で同じように実行されるとは限りません。

    最大容量と、今回扱う個数は別に決まる

    配列に残っている個数など、ソフトウェアが処理したい数をAVL(Application Vector Length)として渡すと、CPUは現在の設定に合わせたvlを返します。ここでのvlは、マスクで要素を除外しない通常のループで、今回処理する個数です。[1]

    たとえばVLMAX=4のとき、残りが3個ならvl=3です。最大4個入るからといって、配列の外まで4個分を処理する必要はありません。

    ただし、vlは常に「残り個数と最大容量の小さい方」になるとは限りません。ここでは基本のV拡張における、通常の要素単位の演算を対象にします。その場合、仕様は次のように選び方を定めています。

    • AVL ≤ VLMAXなら、vl = AVL。
    • AVL ≥ 2 × VLMAXなら、vl = VLMAX。
    • その中間では、AVL ÷ 2を切り上げた値からVLMAXまでの範囲で選べます。

    したがって、最大4個で残り6個なら、最初に4個、次に2個でも、3個ずつ2回でも仕様に適合します。同じ実装で同じAVLとVLMAXを与えたときの結果は決定的ですが、別の実装でも同じ分け方になるとは限りません。プログラムが使うべきなのは、推測した個数ではなく実際に返されたvlです。[1]

    vsetvliで受け取り、その分だけループを進める

    配列を一度に扱える分ずつ切り分ける方法を、ストリップマイニングと呼びます。RVVでは、このループをvsetvli命令で組み立てられます。設定を命令の中に書くvsetvliに対し、vsetvlは要素幅などの設定値を別のレジスタから受け取る命令です。名前は似ていますが、設定の渡し方が違います。[1]

    32ビットの整数配列二つを足す場合、設定命令は次のように書けます。a0に残りの要素数を入れ、返されたvlをt0で受け取ります。

    vsetvli t0, a0, e32, m1, ta, ma

    e32はSEW=32、m1はLMUL=1です。taとmaは、今回の処理対象外となる末尾の要素やマスクで除外した要素の、レジスタ内の値を保持する必要がないことを示します。対象外の値がゼロになるという約束ではありません。この例はマスクを使わず、対象のvl個だけを読み書きします。

    処理全体を、説明用の擬似コードで表すと次のようになります。これは公式の32ビット整数加算例を参考にした手順の説明で、コンパイルできる言語のコードではありません。[2]

    残り個数 = 配列の要素数
    位置 = 0
    残り個数が 0 より大きい間、繰り返す:
        vsetvli で残り個数と e32, m1, ta, ma を渡し、vl を受け取る
        位置から vl 個を読み、対応する要素同士を足して保存する
        位置 = 位置 + vl
        残り個数 = 残り個数 - vl

    実際のメモリアドレスを進めるときは、要素数からバイト数への換算も必要です。32ビットの要素なら1個4バイトなので、入力・出力のアドレスをそれぞれ4 × vlバイト進めます。公式例も、返された個数を残りから引き、バイト数へ換算してポインタを更新しています。

    同じ16個の配列を扱うなら、SEW=32、LMUL=1のもとで、VLEN=128では4個ずつ4回、VLEN=256では8個ずつ2回になります。この場合の回数は、先ほどの仕様の制約から決まります。ソフトウェアが「このCPUは4個ずつ」と埋め込まなくても、同じ手順で全要素を処理できます。

    固定幅SIMDとの違いと、速さを判断する際の注意

    固定幅SIMDでは、たとえば128ビットを扱う命令なら、32ビットの値を4個まとめて扱う、というように命令の幅が基準になります。端数への対応には、命令セットに応じてマスクや別の処理を使います。

    RVVでは、残り個数を渡して処理数を受け取るため、ループを特定のVLENに依存させずに書けます。これがベクトル長に依存しない、vector-length agnosticなコードの考え方です。ただし、必要な要素型と命令を両方のCPUがサポートし、コードも幅を決め打ちしないなどの条件があります。RISC-VのCPUすべてにV拡張が備わる、あるいはどんなベクトルコードでもそのまま移せる、という意味ではありません。[1]

    また、LMULを大きくすると、より多くの要素を一つの命令の対象にできますが、使うレジスタの本数も増えます。32本を8本ずつまとめれば、独立したまとまりは4組です。複数の途中結果を同時に持ちたい処理では、この使い方が制約になります。さらに、命令が対象とする要素を内部の実行器が何サイクルで処理するか、メモリから十分にデータを供給できるかは、最大要素数だけではわかりません。

    CPUやコンパイラの説明を読むときは、まず対応する拡張・要素幅を確認し、次にVLENと実行性能の測定結果を分けて見ます。自分の処理で速さを比べるなら、要素型、配列の長さ、LMUL、メモリ条件をそろえた測定が必要です。本稿の個数とループ回数は仕様からの計算例であり、実行時間の測定値ではありません。

    出典

    1. RISC-V International, The RISC-V Instruction Set Manual, 20260120版、30.1. “V” Standard Extension for Vector Operations, Version 1.0。実装定数、レジスタモデル、SEW・LMUL、vl設定制約、ストリップマイニング。補助照合:固定commitの仕様ソース。
    2. RISC-V International, Vector-vector add example(vvaddint32.s)。上記固定commitの32ビット整数加算例。原資料の利用条件はCC BY 4.0。本文の擬似コードは同例を参考に、変数名・表現を説明用に改めたものです。