RISC-V ベクトル拡張(Vector, RVV)メモ

正式な仕様
- 「RISC-V Technical Specifications」から入手できるUnprivileged ISAのマニュアルに統合されている。
-
Release Vector Extension 1.0, frozen for public review · riscvarchive/riscv-v-spec
- 単体の仕様。2021年にv1.0がリリースとなっている。
書籍
- 「RISC-V原典 | 日経BOOKプラス」(2018年)
- 原著:「The RISC-V Reader: An Open Architecture Atlas」(2017年)
- V拡張の概要が載っている
Web記事等
- 7th RISC-V Workshop の Vector Extension Proposal 概要 - FPGA開発日記(2017年)
- The RISC-V Vector ISA Tutorial(2018年5月)
-
RISC-Vベクトル拡張について解説する - Fixstars Tech Blog /proc/cpuinfo(2019年10月)
- v0.7.1ベース?
- 差分
- v0はゼロレジスターじゃない(「原典」にもv0.7.1にもそういう記述はない)。
- マスクは「各要素の最下位ビット」から「密なビット列」になった(v0.8→v0.9)。
- マスクに指定できるのはv0だけになった(
v0.t
)。v0.7.1の時点でそうなっている。「原典」の時点ではプレディケーション・レジスタとしてvp0, vp1が利用できた(他にもあるが、命令に指定できるのはこの2つ)。
- ARM vs RISC-V Vector Extensions. A comparison of the RISC-V vector… | by Erik Engheim | Medium(2021年4月)
- RISC-VのVector Extensionをざっくりと理解する(2023年2月)
- (動画)RISC-V Vector Extension in Rust - Gijs Burghoorn - Oct 26 2023 - YouTube(2023年10月)
- トーク8分、質疑応答5分
-
Juni's Blog(2024年9月)
- コンパイラー(LLVM, MLIR)の観点にも触れている
C言語へのバインディング
- アナウンス記事「RISC-V Vector Extension Intrinsic Support」(2020年9月)
- riscv-non-isa/rvv-intrinsic-doc
- Release v1.0 - Ratified · riscv-non-isa/rvv-intrinsic-doc
- 呼出規約はどうなる?
注意したい観点
私(@mod_poppo)は言語処理系に興味があるので、コンパイラーや言語設計の観点からベクトル拡張をどう扱えば良いか考えたい。
x86のSIMD事情はある程度知っているので、x86との比較も有益かもしれない。
ArmのSVEとの比較もできると良さそうだが、私はSVEにはそれほど詳しくない。

x86のSIMDではSIMDレジスターの幅が固定で要素数はそれに付随するもの、という感じだが、RVVでは要素数を中心に考えるっぽい。
実装定義のパラメーター
- ELEN: 対応する最大の要素幅(ビット単位)。典型的には32、64や128(128ビット整数や四倍精度浮動小数点数を扱える場合)となりそう。ELEN≥8で、2の冪である必要がある。
- VLEN: 一つのベクトルレジスターの幅(ビット単位)。2の冪で、2^16=65536以下であることが定められている。
- VLEN≥ELENである必要がある。つまり、一つの要素は一つのレジスターに収まることとし、「複数のレジスターを使って一つの要素を表す」ような構成には対応しない。
状態
- 32個のベクトルレジスターv0, …, v31
- 各レジスターはVLENビットの状態を持つ。
- 7個の制御レジスター:vstart, vxsat, vxrm, vcsr, vl, vtype, vlenb
- vtypeはXLEN(典型的には32ビットか64ビット)ビット幅で、vill, vma, vta, vsew, vlmulのビットフィールドを持つ。
- vsew: Vector selected element width。要素の幅を8ビット (
e8
)/16ビット (e16
)/32ビット (e32
)/64ビット (e64
) の中から選択する。典型的には、一つのベクトルレジスターはVLEN/SEW個の要素に分割される。 - vlmul: Vector Register Grouping。複数のレジスターを束ねて一つの大きなレジスターとして使ったり、一つのレジスターの一部だけを使ったりできる。LMUL=1 (
m1
), 2 (m2
), 4 (m4
), 8 (m8
), 1/2 (mf2
), 1/4 (mf4
), 1/8 (mf8
) などの値がサポートされる。
- その他、ベクトルコンテクストに関するもの(状態の退避とかに使うやつ)

ループの構成
例えば、自明に並列化できるループがあったとする:
void float_array_add(size_t n, const float a[n], const float b[n], float result[restrict n])
{
for (size_t i = 0; i < n; ++i) {
result[i] = a[i] + b[i];
}
}
これをベクトル化する時、x86のSSEやAVX、ArmのNEON等では次のような疑似コードになる:
size_t i = 0;
// ベクトルで処理できる部分
for (; i + 4 <= n; i += 4) {
float32x4_t aa = a[i..<i+4]; // load
float32x4_t bb = b[i..<i+4]; // load
float32x4_t cc = aa + bb; // 演算
result[i..<i+4] = cc; // store
}
// 端数の処理
for (; i < n; ++i) {
float aa = a[i]; // load
float bb = b[i]; // load
float cc = aa + bb; // 演算
result[i] = cc; // store
}
このコードで、「本来処理したい要素数」(コード中の n
)はRVVでは「application vector length」(AVL)と呼ばれる。AVLを vset{i}vl{i}
命令に渡すと「ループの現在の周回で処理すべき要素数」(vector length; VL)が得られる。VLは専用のレジスター(vl
)に保管されるほか、汎用レジスターでも受け取ることができる(ループの次の周回までにAVLから減算するのに使う)。
疑似コードで書けば次のようになりそう:
size_t i = 0;
do {
size_t vl = setvl(n - i); // n-iがAVLに相当
float32xN_t aa = a[i..<i+vl]; // load
float32xN_t bb = b[i..<i+vl]; // load
float32xN_t cc = aa + bb; // 演算
result[i..<i+vl] = cc; // store
i += vl;
} while (vl != 0);

要素数の決定
一度に処理できる要素の数は、当然、ハードウェアが提供するベクトルレジスターの幅(VLEN)に依存する。レジスターの幅が大きい方が1命令でより多くの要素を処理できる。
そして、もちろん、処理に使う要素の幅(SEW; e8
/e16
/e32
/e64
)にも依存する。幅が大きい要素型(例:64ビット浮動小数点数)だと1つのレジスターに乗せられる要素数は少なくなるし、幅が小さい要素型(例:8ビット整数)だと1つのレジスターに乗せられる要素数は多くなる。
さらに、処理に必要なベクトルレジスターの個数(register pressureというやつ?)も考慮の対象となる。32個のレジスターをフルに使う複雑な処理であれば32個のレジスターを独立に扱える必要があるが、必要なレジスターの個数が少なければ、複数のレジスターを束ねて1つの巨大なベクトルとして扱えると良さそうである。これを制御するパラメーターがLMULで、「32個のレジスターを独立に使う(LMUL=1 (m1
))」「レジスターを2本ずつまとめて16個のグループに分ける(LMUL=2 (m2
))」「レジスターを4本ずつまとめて8個のグループに分ける(LMUL=4 (m4
))」「レジスターを8本ずつまとめて4個のグループに分ける(LMUL=8 (m8
))」などの選択ができる。
処理の中で複数の幅のデータ型を扱う場合、LMUL=1だと小さな幅のデータを格納したレジスター1本に対して大きな幅のデータを格納するのに2本のレジスターが必要になることがある。しかし、独立したレジスターが大量に必要な複雑な処理だと一つのベクトルにレジスターを2本も使いたくないかもしれない。そういう場合は、LMULを分数値(1/2 (mf2
), 1/4 (mf4
), 1/8 (mf8
))にして、小さな幅のデータを1本のレジスターの一部だけを使って格納しておくと、大きな幅のデータを1本のレジスターに収められる。

x86 SIMDからの移植・WebAssembly SIMDからの変換(128ビットSIMDのRVV上での模倣)
x86の資産をRISC-Vへ移植したり、WebAssemblyの128ビットSIMDをRVVで動かしたいかもしれない。
一文字の「V」で表される拡張(標準的なベクトル拡張)はZvl128bを含意し、Zvl128b拡張はVLENが128以上であることを保証する。なので、「ベクトルレジスターの幅が128ビット以上ある」という条件は普通のV拡張を実装したプロセッサーでは満たされると考えて良い。
128ビットSIMDはデータ型の幅によって要素数が変わる(8ビット整数なら16要素(SEW=8, AVL=16)、64ビット浮動小数点数なら2要素(SEW=64, AVL=2))。RVVで128ビットSIMDをエミュレートしたかったら、扱うデータ型の幅が変わる度にvsetivli命令を発行することになるのだろうか。
自動ベクトル化の例ではループを回していたが、128ビットベクトルの1回の演算のために毎回ループをコード生成していては面白くない。指定されたAVLがそのままVLとして採用される保証があれば1個の演算を1個の命令にコンパイルできる。そして、AVL≤VLMAX(ただしVLMAX=LMUL×VLEN/SEW)であれば VL=AVL であることが保証されている(仕様書6.3)。VLEN≥128で、AVL×SEW=128なので、LMUL≥1とすればこの不等式は確実に成り立つ。

課題
私の現時点の理解で、RVVに対して感じている課題を並べておく。
- レジスター(あるいはそのグループ)を表すデータ型のサイズがコンパイル時に決まらない。イメージとしては
float[VLMAX]
みたいな配列になるが、VLMAXがLMUL, VLEN, SEWに依存して決まるので、これは可変長配列である。ArmのSVEにも似たような問題があり、C言語では特殊なデータ型を用意していた。 - SEWは「扱うデータ型」によって決まり、LMULの最適な値はregister pressureに依存して決まる(私の理解が正しければ)。ループの自動ベクトル化のようにコンパイラーがコード全体を見渡せる場合は良いが、一部の処理を関数に切り出したい場合などに困る気がする。