RISC-V 向量擴展

RISC-V 目標架構支援 RISC-V 向量擴展 (RVV) 1.0 版本。本指南概述了它在 LLVM IR 中如何建模,以及後端如何為其產生程式碼。

對應到 LLVM IR 類型

RVV 新增了 32 個 VLEN 大小的暫存器,其中 VLEN 是編譯器未知的常數。為了能夠表示 VLEN 大小的數值,RISC-V 後端採用與 AArch64 的 SVE 相同的方法,並使用可擴展向量類型

可擴展向量類型採用 <vscale x n x ty> 的形式,表示具有 n 個類型為 ty 的元素的向量倍數。在 RISC-V 上,nty 分別控制 LMUL 和 SEW。

LLVM 僅支援 ELEN=32 或 ELEN=64,因此 vscale 定義為 VLEN/64(請參閱 RISCV::RVVBitsPerBlock)。請注意,這表示 VLEN 必須至少為 64,因此目前不支援 VLEN=32。

LMUL=⅛

LMUL=¼

LMUL=½

LMUL=1

LMUL=2

LMUL=4

LMUL=8

i64 (ELEN=64)

不適用

不適用

不適用

<v x 1 x i64>

<v x 2 x i64>

<v x 4 x i64>

<v x 8 x i64>

i32

不適用

不適用

<v x 1 x i32>

<v x 2 x i32>

<v x 4 x i32>

<v x 8 x i32>

<v x 16 x i32>

i16

不適用

<v x 1 x i16>

<v x 2 x i16>

<v x 4 x i16>

<v x 8 x i16>

<v x 16 x i16>

<v x 32 x i16>

i8

<v x 1 x i8>

<v x 2 x i8>

<v x 4 x i8>

<v x 8 x i8>

<v x 16 x i8>

<v x 32 x i8>

<v x 64 x i8>

double (ELEN=64)

不適用

不適用

不適用

<v x 1 x double>

<v x 2 x double>

<v x 4 x double>

<v x 8 x double>

float

不適用

不適用

<v x 1 x float>

<v x 2 x float>

<v x 4 x float>

<v x 8 x float>

<v x 16 x float>

half

不適用

<v x 1 x half>

<v x 2 x half>

<v x 4 x half>

<v x 8 x half>

<v x 16 x half>

<v x 32 x half>

(將 <v x k x ty> 解讀為 <vscale x k x ty>

遮罩向量類型

遮罩向量在物理上使用向量暫存器中緊密封裝的位元佈局來表示。它們對應到以下 LLVM IR 類型

  • <vscale x 1 x i1>

  • <vscale x 2 x i1>

  • <vscale x 4 x i1>

  • <vscale x 8 x i1>

  • <vscale x 16 x i1>

  • <vscale x 32 x i1>

  • <vscale x 64 x i1>

具有相同 SEW/LMUL 比率的兩種類型將具有相同的相關遮罩類型。例如,在 SEW=64、LMUL=2 和另一個在 SEW=32、LMUL=1 下的兩個不同比較都將產生遮罩 <vscale x 2 x i1>

在 LLVM IR 中的表示法

向量指令可以在 LLVM IR 中以三種主要方式表示

  1. 可擴展和固定長度向量類型上的常規指令

    %c = add <vscale x 4 x i32> %a, %b
    %f = add <4 x i32> %d, %e
    
  2. RISC-V 向量內建函數,它鏡像 C 內建函數規範

    這些有未遮罩變體

    %c = call @llvm.riscv.vadd.nxv4i32.nxv4i32(
           <vscale x 4 x i32> %passthru,
           <vscale x 4 x i32> %a,
           <vscale x 4 x i32> %b,
           i64 %avl
         )
    

    以及遮罩變體

    %c = call @llvm.riscv.vadd.mask.nxv4i32.nxv4i32(
           <vscale x 4 x i32> %passthru,
           <vscale x 4 x i32> %a,
           <vscale x 4 x i32> %b,
           <vscale x 4 x i1> %mask,
           i64 %avl,
           i64 0 ; policy (must be an immediate)
         )
    

    兩者都允許設定 AVL 以及透過 passthru 運算元控制非活動/尾部元素,但遮罩變體也為遮罩和 vta/vma 策略位元提供運算元。

    唯一有效的類型是可擴展向量類型。

  3. 向量述詞 (VP) 內建函數

    %c = call @llvm.vp.add.nxv4i32(
           <vscale x 4 x i32> %a,
           <vscale x 4 x i32> %b,
           <vscale x 4 x i1> %m
           i32 %evl
         )
    

    與 RISC-V 內建函數不同,VP 內建函數與目標架構無關,因此它們可以從中端的其他優化 Pass(如迴圈向量化器)發出。它們也支援固定長度向量類型。

    VP 內建函數也沒有 passthru 運算元,但尾部/遮罩不受干擾行為可以透過在 @llvm.vp.merge 中使用輸出進行模擬。它將被降低為 vmerge,但將透過 RISCVDAGToDAGISel::performCombineVMergeAndVOps 合併回底層指令的遮罩中。

上述表示法的不同屬性總結如下

AVL

遮罩

Passthru

可擴展向量

固定長度向量

目標架構無關

LLVM IR 指令

始終 VLMAX

RVV 內建函數

VP 內建函數

是 (EVL)

SelectionDAG 降低

對於大多數常規的可擴展向量 LLVM IR 指令,它們對應的 SelectionDAG 節點在 RISC-V 上是合法的,不需要任何自訂降低。

t5: nxv4i32 = add t2, t4

RISC-V 向量內建函數也不需要任何自訂降低。

t12: nxv4i32 = llvm.riscv.vadd TargetConstant:i64<10056>, undef:nxv4i32, t2, t4, t6

固定長度向量

由於沒有固定長度向量模式,因此固定長度向量需要自訂降低,並在可擴展的「容器」類型中執行

  1. 固定長度向量運算元透過 insert_subvector 節點插入到可擴展容器中。選擇容器類型,使其最小尺寸將適合固定長度向量(請參閱 getContainerForFixedLengthVector)。

  2. 然後透過 VL(向量長度)節點在容器類型上執行操作。這些是在 RISCVInstrInfoVVLPatterns.td 中定義的自訂節點,它鏡像目標架構無關的 SelectionDAG 節點,以及一些 RVV 指令。它們包含一個 AVL 運算元,該運算元設定為固定長度向量中的元素數量。某些節點還具有 passthru 或遮罩運算元,在降低固定長度向量時,它們通常會設定為 undef 和全 1。

  3. 結果透過 extract_subvector 放回固定長度向量中。

    t2: nxv2i32,ch = CopyFromReg t0, Register:nxv2i32 %0
    t6: nxv2i32,ch = CopyFromReg t0, Register:nxv2i32 %1
  t4: v4i32 = extract_subvector t2, Constant:i64<0>
  t7: v4i32 = extract_subvector t6, Constant:i64<0>
t8: v4i32 = add t4, t7

// is custom lowered to:

    t2: nxv2i32,ch = CopyFromReg t0, Register:nxv2i32 %0
    t6: nxv2i32,ch = CopyFromReg t0, Register:nxv2i32 %1
    t15: nxv2i1 = RISCVISD::VMSET_VL Constant:i64<4>
  t16: nxv2i32 = RISCVISD::ADD_VL t2, t6, undef:nxv2i32, t15, Constant:i64<4>
t17: v4i32 = extract_subvector t16, Constant:i64<0>

VL 節點通常具有 passthru 或遮罩運算元,對於固定長度向量,它們通常設定為 undef 和全 1。

負責封裝和解封裝的 insert_subvectorextract_subvector 節點將被合併消除,最終我們將把所有固定長度向量類型降低為可擴展類型。請注意,函數介面處的固定長度向量在可擴展向量容器中傳遞。

注意

唯一通過降低的 insert_subvectorextract_subvector 節點是那些可以作為精確子暫存器插入或提取執行的節點。這表示任何未合法化的固定長度向量 insert_subvectorextract_subvector 節點都必須位於暫存器組邊界上,因此必須在編譯時知道精確的 VLEN(即,使用 -mrvv-vector-bits=zvl-mllvm -riscv-v-vector-bits-max=VLEN 編譯,或具有精確的 vscale_range 屬性)。

向量述詞內建函數

VP 內建函數也透過 VL 節點進行自訂降低。

t12: nxv2i32 = vp_add t2, t4, t6, Constant:i64<8>

// is custom lowered to:

t18: nxv2i32 = RISCVISD::ADD_VL t2, t4, undef:nxv2i32, t6, Constant:i64<8>

VP EVL 和遮罩分別用於 VL 節點的 AVL 和遮罩,而 passthru 則設定為 undef

指令選擇

vlvtype 需要正確配置,因此我們不能直接選擇底層向量 MachineInstr。而是選擇偽指令,這些偽指令攜帶稍後發出必要 vsetvli 所需的額外資訊。

%c:vrm2 = PseudoVADD_VV_M2 %passthru:vrm2(tied-def 0), %a:vrm2, %b:vrm2, %vl:gpr, 5 /*sew*/, 3 /*policy*/

每個向量指令在 RISCVInstrInfoVPseudos.td 中定義了多個偽指令。每個可能的 LMUL 都有每個偽指令的變體,以及遮罩變體。因此,像 vadd.vv 這樣的典型指令將具有以下偽指令

%rd:vr = PseudoVADD_VV_MF8 %passthru:vr(tied-def 0), %rs2:vr, %rs1:vr, %avl:gpr, sew:imm, policy:imm
%rd:vr = PseudoVADD_VV_MF4 %passthru:vr(tied-def 0), %rs2:vr, %rs1:vr, %avl:gpr, sew:imm, policy:imm
%rd:vr = PseudoVADD_VV_MF2 %passthru:vr(tied-def 0), %rs2:vr, %rs1:vr, %avl:gpr, sew:imm, policy:imm
%rd:vr = PseudoVADD_VV_M1 %passthru:vr(tied-def 0), %rs2:vr, %rs1:vr, %avl:gpr, sew:imm, policy:imm
%rd:vrm2 = PseudoVADD_VV_M2 %passthru:vrm2(tied-def 0), %rs2:vrm2, %rs1:vrm2, %avl:gpr, sew:imm, policy:imm
%rd:vrm4 = PseudoVADD_VV_M4 %passthru:vrm4(tied-def 0), %rs2:vrm4, %rs1:vrm4, %avl:gpr, sew:imm, policy:imm
%rd:vrm8 = PseudoVADD_VV_M8 %passthru:vrm8(tied-def 0), %rs2:vrm8, %rs1:vrm8, %avl:gpr, sew:imm, policy:imm
%rd:vr = PseudoVADD_VV_MF8_MASK %passthru:vr(tied-def 0), %rs2:vr, %rs1:vr, mask:$v0, %avl:gpr, sew:imm, policy:imm
%rd:vr = PseudoVADD_VV_MF4_MASK %passthru:vr(tied-def 0), %rs2:vr, %rs1:vr, mask:$v0, %avl:gpr, sew:imm, policy:imm
%rd:vr = PseudoVADD_VV_MF2_MASK %passthru:vr(tied-def 0), %rs2:vr, %rs1:vr, mask:$v0, %avl:gpr, sew:imm, policy:imm
%rd:vr = PseudoVADD_VV_M1_MASK %passthru:vr(tied-def 0), %rs2:vr, %rs1:vr, mask:$v0, %avl:gpr, sew:imm, policy:imm
%rd:vrm2 = PseudoVADD_VV_M2_MASK %passthru:vrm2(tied-def 0), %rs2:vrm2, %%rs1:vrm2, mask:$v0, %avl:gpr, sew:imm, policy:imm
%rd:vrm4 = PseudoVADD_VV_M4_MASK %passthru:vrm4(tied-def 0), %rs2:vrm4, %rs1:vrm4, mask:$v0, %avl:gpr, sew:imm, policy:imm
%rd:vrm8 = PseudoVADD_VV_M8_MASK %passthru:vrm8(tied-def 0), %rs2:vrm8, %rs1:vrm8, mask:$v0, %avl:gpr, sew:imm, policy:imm

注意

雖然 SEW 可以編碼在運算元中,但我們需要為每個 LMUL 使用單獨的偽指令,因為不同的暫存器組將需要不同的暫存器類別:請參閱暫存器分配

偽指令具有 AVL 和 SEW(編碼為 2 的冪)的運算元,以及可能的遮罩、策略或捨入模式(如果適用)。passthru 運算元與目標暫存器綁定,該暫存器將決定非活動/尾部元素。

對於應使用 VLMAX 的可擴展向量,AVL 設定為 -1 的哨兵值。

RISCVInstrInfoVSDPatterns.td 中有針對目標架構無關 SelectionDAG 節點的模式,RISCVInstrInfoVVLPatterns.td 中有 VL 節點的模式,RISCVInstrInfoVPseudos.td 中有 RVV 內建函數的模式。

僅對遮罩進行操作的指令(如 VMAND 或 VMSBF)使用帶有 B1、B2、B4、B8、B16、B32 或 B64 後綴的偽指令,其中數字是 SEW/LMUL,表示 vtype 中所需的 SEW 和 LMUL 之間的比率。這些指令始終像 EEW=1 一樣運作,並且始終使用值 0 作為其 SEW 運算元。

遮罩模式

對於遮罩偽指令,遮罩運算元在指令選擇期間使用黏合的 CopyToReg 節點複製到物理 $v0 暫存器

  t23: ch,glue = CopyToReg t0, Register:nxv4i1 $v0, t6
t25: nxv4i32 = PseudoVADD_VV_M2_MASK Register:nxv4i32 $noreg, t2, t4, Register:nxv4i1 $v0, TargetConstant:i64<8>, TargetConstant:i64<5>, TargetConstant:i64<1>, t23:1

RISCVInstrInfoVVLPatterns.td 中的模式僅將遮罩偽指令與之匹配,以減小匹配表的大小,即使節點的遮罩全為 1 並且可以是未遮罩的偽指令。RISCVFoldMasks::convertToUnmasked 將檢測遮罩是否全為 1,並將其轉換為未遮罩形式。

$v0 = PseudoVMSET_M_B16 -1, 32
%rd:vrm2 = PseudoVADD_VV_M2_MASK %passthru:vrm2(tied-def 0), %rs2:vrm2, %rs1:vrm2, $v0, %avl:gpr, sew:imm, policy:imm

// gets optimized to:

%rd:vrm2 = PseudoVADD_VV_M2 %passthru:vrm2(tied-def 0), %rs2:vrm2, %rs1:vrm2, %avl:gpr, sew:imm, policy:imm

注意

任何 vmset.m 都可以視為全 1 遮罩,因為超出 AVL 的尾部元素是 undef 並且可以用 1 替換。

暫存器分配

暫存器分配在向量和純量暫存器之間分配,向量分配首先執行

$v8m2 = PseudoVADD_VV_M2 $v8m2(tied-def 0), $v8m2, $v10m2, %vl:gpr, 5, 3

注意

暫存器分配被拆分,以便 RISCVInsertVSETVLI 可以在向量暫存器分配之後但在純量暫存器分配之前執行。它需要在純量暫存器分配之前執行,因為它可能需要建立新的虛擬暫存器以將 AVL 設定為 VLMAX。

在向量暫存器分配之後執行 RISCVInsertVSETVLI 對機械碼排程器施加的約束更少,因為它無法排程指令通過 vsetvli,並且它允許我們在溢出或常數重新實體化期間發出進一步的向量偽指令。

向量有四個暫存器類別

  • VR 用於向量暫存器(v0v1,、…、v32)。當 \(\text{LMUL} \leq 1\) 和遮罩暫存器時使用。

  • VRM2 用於長度為 2 的向量組,即 \(\text{LMUL}=2\)v0m2v2m2、…、v30m2

  • VRM4 用於長度為 4 的向量組,即 \(\text{LMUL}=4\)v0m4v4m4、…、v28m4

  • VRM8 用於長度為 8 的向量組,即 \(\text{LMUL}=8\)v0m8v8m8、…、v24m8

\(\text{LMUL} \lt 1\) 類型和遮罩類型不會從擁有專用類別中受益,因此在它們的情況下使用 VR

某些指令具有暫存器運算元不能是 V0 或與 V0 重疊的約束,因此對於這些情況,我們還有 VRNoV0 變體。

RISCVInsertVSETVLI

在分配向量暫存器後,RISCVInsertVSETVLI Pass 將為偽指令插入必要的 vsetvli

dead $x0 = PseudoVSETVLI %vl:gpr, 209, implicit-def $vl, implicit-def $vtype
$v8m2 = PseudoVADD_VV_M2 $v8m2(tied-def 0), $v8m2, $v10m2, $noreg, 5, implicit $vl, implicit $vtype

物理 $vl$vtype 暫存器由 PseudoVSETVLI 隱式定義,並由 PseudoVADD 隱式使用。vtype 運算元(本例中為 209)根據規範透過 RISCVVType::encodeVTYPE 進行編碼。

RISCVInsertVSETVLI 執行資料流分析,以盡可能少地發出 vsetvli。它還將嘗試最小化設定 VL 的 vsetvli 數量,即,如果僅需要更改 vtype 但不需要更改 vl,它將發出 vsetvli x0, x0

偽指令展開和列印

在純量暫存器分配之後,RISCVExpandPseudoInsts.cpp Pass 展開 PseudoVSETVLI 指令。

dead $x0 = VSETVLI $x1, 209, implicit-def $vtype, implicit-def $vl
renamable $v8m2 = PseudoVADD_VV_M2 $v8m2(tied-def 0), $v8m2, $v10m2, $noreg, 5, implicit $vl, implicit $vtype

請注意,向量偽指令仍然保留,因為它需要為 LMUL 編碼暫存器類別。它的 AVL 和 SEW 運算元不再使用。

RISCVAsmPrinter 然後將偽指令降低為真實的 MCInst

vsetvli a0, zero, e32, m2, ta, ma
vadd.vv v8, v8, v10

另請參閱