RISC-Vベクトル拡張について解説する

全国のRISC-Vファンの方、こんにちは。

今日は、RISC-Vの”V”の本丸機能であるVector extension(ベクトル拡張)、略してRVVがどのようなものか、解説をしたいと思います。

(「なんでフィックスターズがRISC-V?」などは前回のRISC-V記事をご覧ください)

はじめに注意

RV32Vはまだ正式版(stable)ではありません。執筆時点の最新リリースバージョンはv0.7.1です。

現在もmasterにどんどん更新が入っています。
とはいえ、聞き及んだところによると、バイナリ表現などが決まっては居ないものの、レジスタや命令については概ね完成しているようです。

本記事では、執筆時点にRISC-V “V” Vector Extensionのページで確認した最新版である”0.8-draft-20190906″に準拠した解説をします。

また、この記事にはRISC-Vの仕様がかなり多く含まれている関係上、本家レポジトリと同じクリエイティブ・コモンズ表示4.0国際ライセンスの元で利用許諾します。再利用時の帰属表示には、本家に加えて「株式会社フィックスターズ」を追加してください。

クリエイティブ・コモンズ・ライセンス

ベクトル一般論

「ベクトル」と聞いてみなさん何を思い浮かべますか?

  • 高校で習ったなんか矢印みたいなやつ
  • 1階のテンソル
  • 老舗のフリーソフトサイト

など色々あると思いますが、本記事で言う「ベクトル」とは、1命令で複数のデータを処理する方法の1つを指します。
複数の処理を並列実行するので、ソフトウェアの高速化には大変重要な概念です。

もしかして、ベクトル命令アーキテクチャについてご存じないですか?ではヘネパタ読んでください(『RISC-V原典』p.75より)。

しかし現代のIT技術者であってもベクトル命令が使える計算機使ったことある人の方が稀なので(『RISC-V原典』p.75より)、少し簡単に+あえて詳細には触れず雰囲気を知ってもらうぐらい紹介を以下にします。

なお、RISC-Vの解説記事であることから分かる通り、以降では前提として、話は命令セットアーキテクチャ(ISA)のことを指しており、ハードウェアの実装アーキテクチャの話ではないことに注意してください。

SIMDとの違い

多くの方は「1命令で複数データを実行」と聞いて、x86, x64にあるSSEやAVXを思い浮かべることと思います。
これらの命令セットアーキテクチャを、RISC-Vの文脈では(特に『RISC-V原典』では)、「SIMDアーキテクチャ」と呼んでいます。
一方で、RISC-Vでは同様の「1命令で複数データを実行」する手法として、SIMDではなく「ベクトル」を採用しています。

ベクトルがSIMDと違う最大の点は、固定長でないことです。
SSEやAVXやNEONなど有名なSIMD命令は全て「どの命令が何bitか」決まっていますが、それだとハードウェアが進化して幅が増えたり変わったりするたびに新しい命令を作ることになり、ISAが不安定になります。

そのため、「ISAはシンプルかつ安定的であるべき」という思想のあるRISC-Vでは、SIMDではなくベクトルを使うことになりました。

この辺り、用語が分かりにくい上に色んな人や企業が色んな流儀で呼んでいるので混乱しやすいのですが、以降はとりあえずこのRVVの流儀に従って、このように区別することにします。

現代のベクトル・アーキテクチャ

ちょっと話は脱線しますが、なぜ現代ではベクトルではなくSIMDが主流になっているのかを整理します。

一番大きい理由は、我々が普段使っているCPUの最大手であるIntelらがMMX, SSE, AVXとSIMD推しだからだと思われます。
ではなぜそれらのCPUでベクトルが採用されていないか?というと、当事者らに聞かないと本当のことは分かりませんが、1つの理由には、大きなベクトルレジスタではコンテキストスイッチのコストが高いことが挙げられるようです。
コンテキストスイッチの時には、現在のレジスタの値を全部保存して切り替える必要があり、長いレジスタ幅だとそこがかなり重くなってしまいます(『RISC-V原典』p.78)。

という事情から、ベクトルは本当の汎用計算機には向かず、それが普通のCPUを使った計算機には採用されていない一つの理由のようです。

一方、現代でも、主にHPC用途ではベクトル・アーキテクチャはまだ現役です(GPGPUのようなメニーコアSIMTにかなり圧されている気はしますが)。
例えば、NECのSX-Aurora TSUBASA(※ツバメではない)はつい最近発売された、代表的なベクトルアーキテクチャを採用したハードウェアですね。
あと、有名なところでは、ポスト京あらため富岳で使われるA64fxが採用するARM SVE(Scalable Vector Extension)という命令セットがベクトルとなっています。
※SVEがベクトルかSIMDかについては異論もあります

RISC-Vでも、ベクトル拡張を推進している人たちは、RISC-V総本山のUCバークレーの人に加えて、超並列RISC-Vコアを作ろうとしているEsperanto Technologiesの人がいたり、割とHPC向きの人が多いようです。

ベクトル計算機の仕組み

先に書いた通り、SIMDとの大きな違いは、1命令で実行できる長さが可変であることです。これはどう実現しているでしょうか?

簡単に想像がつくと思いますが、1命令で実行できる長さレジスタがあり、計算したい長さだけ演算が繰り返される命令です。つまり擬似コードでかけば

という処理がベクトルでは

なります。SIMDとの違いは、Lが命令オペランドにあるのか、実行時に読み込むのかという違いとも言えますね。
しかし、この命令オペランドにあるかどうかは、命令デコーダー部に関わってくるので、ハードウェア的には大きな違いがあります。

また、これは本当にハードウェア的にもL個同時に実行されていることを必ずしも意味してはいません。
ハードウェア上は、中で1要素ずつL回反復が回っているかもしれませんし、L/2回ずつかもしれません。
しかし仮に同時実行でないとしても、各処理が独立していることが保証されているのできれいにパイプライン処理でき、実行時間の短縮(効率化)につながります。

という感じで、ハードウェアの実装と命令をうまく分離できているのがベクトル命令アーキテクチャの特徴です。

一方で、見て分かる通り、これでは実行レイテンシがどうなっているかが、命令だけでは決まりません。その辺りの予測困難さ(に加えて、先ほどのコンテキストスイッチの重さ)がSIMDと比較したときのトレードオフになります。

RV32V

さて軽く復習したところで、本編です。

RISC-Vにおけるベクトル命令規格はRV32V(またはRV64V)で定義されています。命令は概ね

  • 設定命令
  • 整数命令
  • 固定小数命令
  • 浮動小数命令
  • 読み書き命令
  • 縮約命令
  • マスク命令
  • 交換命令

に分けられます。また、これらに加えて、ベクトル演算をするためのレジスタが用意されています。

RV32Vでは、普通のベクトル・アーキテクチャよりも更にISAを単純化するために大変工夫しています。
このベクトル拡張の策定・推進にはかなりの労力のかけられているようで、『RISC-V原典』にはRISC-VのVはVectorのVとも言える、とまで書いてあります。

レジスタと設定命令

ここが割と重要なRISC-Vの工夫なので、少し詳細に説明します。

レジスタ

まず、RV32Vに定義されているレジスタには

  • v0-v31:ベクトルレジスタ
  • vl:ベクトル長
  • vtype:データ型
  • vxsat:固定小数の飽和フラグ
  • vxrm:固定小数の丸めモード
  • vstart:ベクトル演算の開始オフセット

があります。最初のだけが汎用レジスタ、残りはシステム制御用レジスタ(CSR/Control and Status Registers)です。そして、後者3つは細かい設定を必要とする時にしか使わないので、本解説では割愛します。大事なのは前者3つです。

まず、ベクトルレジスタが32個ありますが、幅は実装依存です。これはVLENと呼ばれ、2ベキであることと、全てのレジスタが同じVLENであることのみが保証されています。
ただしv0はゼロレジスタであり、常に0で、何を書いても0のままです(スカラーのx0レジスタと同じです)。

vlレジスタには、実際に1命令で実行されるベクトル長、つまり、何個の要素が計算されるかを符号なし整数で示します。

vtypeレジスタは、以下のように、1要素の長さ等を各フィールドで表します

  • vsew:3[bit]の値で、1要素の長さSEWが2^(3+vsew)[bit]であることを表します(例えばb011なら1要素は64bit)。SEWの最小値は8、最大値は1024になります
  • vlmul:2[bit]の値で、1命令で使用するベクトルレジスタの数LMULが2^vlmul個であることと表します(例えばb10なら、指定されたレジスタから後ろ4個が使われる)。LMULに最小値は1、最大値は8になります
  • vill:設定値がおかしい時に1になるフラグです(詳しくは後述)
  • vediv:EDIVと呼ばれる拡張機能で使うのですが、拡張機能なので本解説では割愛します

vlとvtypeレジスタによって、1命令で実行できる最大要素数VLMAXVLEN*LMUL/SEW個であると決定されます。

設定命令

このvlとvtypeレジスタは読み込み専用で、設定はレジスタに直接書くのではなく、vsetvliまたはvsetvl命令で同時に指定します。

  • rs1は、vlに設定しようとする値AVLです
  • vtypeiには、vtypeレジスタの値をそのまま即値指定します。即値のために、アセンブラには以下が用意されています
    • e8,e16,…,e128→SEWが8,16,…,128であることを示す
    • m1,m2,m4,m8→LMULが1,2,4,8個であることを示す(指定しないとm1とみなされる)
  • vsetvlでは即値ではなくレジスタrs2でvtypeを値を指定できます
  • rdには、実際にvlとなった値が返ってきます

ここで注意しなければならないのは、rs1で指定した値が、そのまま計算で使用されるベクトル長vlになるわけではないということです。
簡単な例を出すと、32[bit]要素を128個演算したい(SEW=32、AVL=128)と指定すると、全部で32*128=4096[bit]必要になりますが、ベクトルレジスタの数は31個しかないのでベクトルレジスタの幅VLENが64[bit]の実装だった場合には、1命令で実行できる要素数vlが128個になることはありません(レジスタ全部合わせて32*64=1984[bit]しかないので、4096[bit]は入らない)

指定された要素数AVLから実際に計算されるvlの値は実際には以下のように決まります

  • VLMAX以下の時は、AVLそのまま (AVL≦VLMAX→vl=AVL)
  • VLMAXの2倍より小さい時は、半分(切り上げ)からVLMAXの間 (VLMAX<AVL<2*VLMAX→⌈AVL/2⌉≦vl≦VLMAX)
  • VLMAXの2倍以上なら、VLMAX (AVL≧2*VLMAX→vl=VLMAX)
  • -1なら無限を意味し、つまり最大値を使うことを意味します (AVL=-1→vl=VLMAX)

なお、AVLに0を指定すると、vlの値を変更することなく、rdに現在のvl値を取得できます。

少し複雑なので、例題を出します。
ここに、ベクトルレジスタの最大幅VLENが2048[bit]のハードウェアがあるとします。それに対して、64[bit]要素の配列をベクトル演算したい時には、

  1. まず、vtypeiまたはrs2のvsewフィールドにはb011を指定し、1要素の幅SEWを64[bit]にします
  2. 1命令でいくつのレジスタを使うかをvlmulフィールドで決めます。ここではb10を指定し、1命令で4つのレジスタを使用するLMUL=4としました
  3. すると、1命令で実行できる最大要素数VLMAXは4*2048/64=128個になります
  4. なので、rs1でAVL=256以上を指定すれば、1命令で実際に実行される要素数vlは128個になります
  5. AVL=205を指定すると、vlは103個から128個のどれか(実装依存)になります
  6. AVL=18を指定すると、vl=18個になります
  7. AVL=-1を指定すると、最大値なのでvl=128個になります
  8. AVL=0を指定すると、vlの値は変わらないまま、rdに現在のvlが格納されます

という感じになります。

このようにvlの決定方法が決められているのは、主にレジスタスピルやコンテキストスイッチのために退避・復元するレジスタ数などを決定しやすくするためです。
また、「VLMAXの2倍より小さい時」の特殊化が入っているのは、反復の最後の端数処理の時のためです。
例えば1命令の最大要素数(VLEN)=128個で残りの要素数(AVL)=140個の時に、最後の2回の反復で計算する要素数を、128個→12個と偏らせず70個→70個と均等分散させられます。

また、vtypeにはどんな値でも入れられるわけではなく、要素の長さSEWには制限があり、これをELENと呼び実装依存の固有値です(2ベキではある)。
例えば、スカラーのレジスタ幅(※RV32やRV64などのアドレス幅とは関係ない)であるXLENより長いデータ長はサポートしないことが多いと考えられるので、その場合ELENは自動的にXLENと等しくなることが多いと思います。
もし、SEWなどに実装でサポートされていない値を入れた場合、villフィールドに1が立ちます。

このような仕様の何が嬉しいかと言うと、ISAがかなり単純になることです。先述の通り、従来は、演算したい幅や要素幅をオペコードに入れていたため、それぞれに対して命令が定義され、結果的にデコーダーの負荷が高く、かつ将来の拡張性が低いものでした。

しかし、RV32Vなら、以下のようにとても簡単な命令列で、どんなハードウェアでも効率的にベクトル演算をすることができます。

マスク

多くの命令では、オペランドにマスクvmを指定し、命令を実行する要素としない要素を選択できます。

マスクに用いるのは、各要素の最下位ビットのみで、1ならば実行され、0ならば実行されません。
vmにv0(ゼロレジスタ)を入れると1つも実行されないことになります。逆に、全部実行したい(ことのほうが多いと思いますが)場合は、アセンブラには何も指定しないでください(全てが1になります)。
また、普通のレジスタとマスクを区別するために、アセンブラ上ではvmのところに指定したレジスタには.tと書くことになっています

マスクに用いる値を用意するために、レジスタの最下位ビットだけを操作するマスク専用命令vm***があります。

vmand vd = vs2 & vs1
vmandnot vd = vs2 & !vs1
vmnand vd = !(vs2 & vs1)
vmor vd = vs2 | vs1
vmornot vd = vs2 | !vs1
vmnor vd = !(vs2 | vs1)
vmxor vd = vs2 ^ vs1
vmxnor vd = !(vs2 ^ vs1)

その他に、実行される要素(1)の数を数える(いわゆるポップカウント)や、最初の要素番号を調べる命令もあります

また、「最初に1が立っているところ」に対して、「それより前を全て1にする」「自分も含めて1にする」「自分だけ1にする」という命令もあります

命令の結果のvd/vs2 0 0 0 1 0 1 1 0
vmsbf 1 1 1 0 0 0 0 0
vmsif 1 1 1 1 0 0 0 0
vmsof 0 0 0 1 0 0 0 0

メモリからの読み書き

ベクトルレジスタに何かしらの値を持ってくるために、メモリから読み書きする必要があります。RV32Vでは、以下3種類のメモリアクセスが可能です

  • 通常の連続アクセス:vl/vs命令
  • ストライドアクセス:vls/vss命令
  • インデックスアクセス:vlx/vsx命令

※vlがvector lengthのレジスタと、vector loadの読み込み命令の両方の意味が使われるので注意

これらは、機械語レベルでは1つのオペコード0000111(load)と0100111(store)が割り当てられており、フィールドの値を変えることで切り替えられます。

連続アクセス

通常の連続アクセスは簡単で、指定したアドレスからvl個の要素をLMUL個のレジスタを使って読み込みます

ストライド

ストライドは、連続アクセスに加えて、rd2にストライド、つまり次の要素までの間隔が何バイトであるかを指定します(負またはゼロも可)

インデックスアクセス

インデックスアクセスは、固定されたストライドではなく、各要素が指定されたベクトルレジスタにある要素番号(オフセット)から取得されます

フラグ判定付き読み込み

連続アクセスには、「0が取得されたらそこで読み書きを終了する」という読み込み命令(fault-only-first)があります。

この命令を実行後にvlレジスタを読みに行くと、実際に読み込まれた値が含まれています。

つまり、例えばvl=8の時にvd={10, 11, 12, 13, 14, 15, 16, 17}に対してvlffを発行した時、(rs1)に{1, 2, 3, 4, 5, 0, 0, 0}があったとすると、実際にはvd={1, 2, 3, 4, 5, 15, 16, 17}になり、vl=5になっています。

これは、whileループのようなもので、特に\0終端の文字列を読み込む時などに有用です。

なお、フラグ判定付きは連続アクセスにしかなく、ストライドやインデックスアクセスには用意されていません。これは、オーバーラン等のセキュリティーリスクが高すぎるためとのことです。将来的に拡張機能で入る可能性は否定されていないようです。

(拡張機能)セグメント読み書き

上記までは1要素ずつの読み書きでしたが、拡張機能として、複数要素の単位で読み書きする命令が提案されています。

これは、AoS(Array of Structures)のデータ構造を読み書きするのに重要で、特にストライドやインデックスアクセスに利用されます。

まだ提案されたばかり&拡張機能なので、詳しくは割愛します。

演算命令(整数・固定小数・浮動小数)

  • 整数命令
  • 固定小数命令
  • 浮動小数命令

は、それぞれ、その形式での演算を実行します。
先述の通り、要素幅(何ビットか)はSEWで決まっているので、これらの違いは、その幅の数字を、どのように扱うかという点しか違いがありません。

どの形式においても、基本的な加減乗除はもちろん、対応するビット演算、根号、FMAなどもあります。

固定小数命令は、多くの演算は整数命令で代替できますが、以下のような命令が追加されます。

  • CSRレジスタで少し触れた、飽和や丸めモードを使う演算
  • 演算後に小数点を移動(スケール)させる演算

があります。

浮動小数命令の場合、IEEE754/2008には16,32,64,128bitしか規定がないので、SEWにはこのうちいずれか(かつ、実装固有値で最大幅であるELEN以下)でないと、不正命令例外になります。

また、いくつかの命令では、演算結果の幅を倍にしたり半分にしたりすることも可能で、特に加算・乗算においては、オーバーフロー等を防ぐことができます。

縮約命令

縮約(リダクション、reduction)は、vl個の値を1つにまとめるものです。

  • 総和
  • 最大
  • 最小
  • 論理積
  • 論理和
  • 排他的論理和

が定義されています。どの形式でも

という形になっており、***にsum, orなど具体的な演算名が入ります。この命令では、vdの0要素目に、vs1の0要素目とvs2全部の縮約結果が入ります。

演算結果を幅を倍にする命令や、浮動小数演算の場合は順番を保証するものとしない2種類が用意されています。

交換命令

交換命令では、ベクトルレジスタ内の要素を並び替えたり移動させたりすることができます。

いずれも、移動先(vd)と元(vs2)を重ねると不正命令となります。どうしても重なる場合はちゃんとマスクしてください。

水平移動

n個ずらして格納します

また、1個だけずらし、末尾/先頭にはスカラーレジスタx0,x1,…から読み込むという命令もあります。

インデックス移動

ベクトルレジスタにコピー元の要素番号を入力することができます

圧縮

フラグの立っている要素のみを複製し、前に詰める命令です

例えば以下のようになります

vs1 0 1 0 1 1 0 1 1
vs2 1 2 3 4 5 6 7 8
vd 2 4 5 7 8 0 0 0

スカラー・ベクトル変換

ベクトルレジスタの特定要素をスカラーレジスタに移動させたり、元に戻したりできます

終わりに

最初に書いた通り、ベクトル命令に関する規格はまだ大きく変わっており、この内容も将来的に変更される可能性があります。ただし、基本的な考え方はあまり変わっていないはずで、教科書も副読本としては価値があると思います(が、なくなったり名前が変わったりしているものも多いので・・・)。

いつかベクトル命令が安定版になる日を待ち望んでいます!!

付録:『RISC-V原典』と最新規格の差異

RISC-Vを学ぼうと思う方は、『RISC-V原典』を手に取り読み始める方が多いかもしれませんが、散々述べてきた通りベクトル拡張は日々変化しているので、書籍に書かれている内容から大きく変わっています。
以下、気づいたところを列挙しておきます(全部網羅したわけではありません)。

  • 最大ベクトル長mvlはなくなり、vlやVLENなどに変更された
  • プレディケーションレジスタvpiはなくなり、汎用レジスタでマスクを取り扱うことになった
  • 要素の型や長さを指定するvsetdcfgやベクトル長を指定するsetvl命令はなくなり、vsetvli命令に統合され同時に指定することになった
  • 読み書きの命令名がvld/vstからvl/vsに変わった
  • ベクトルのインデックス置換はvselectではなくなり、vrgatherになった

参考文献

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください