RISC-V与LLVM Intrinsics函数

RISC-V与LLVM Intrinsics函数

RISC-V向量扩展支持Intrinsics

RISC-V矢量扩展(RVV)使基于RISC-V指令集架构的处理器内核能够处理数据阵列,以及传统的标量运算,以加速对大型数据集的单个指令流计算。

RISC-V国际协会矢量工作组由来自行业和学术界的专家组成,该工作组旨在创建可供任何选择使用RVV的人所普遍采用的矢量扩展标准。

我们非常高兴地宣布,来自SiFive和巴塞罗那超级计算中心的团队已经合作创建了一个新的应用程序接口(API),支持RISC-V矢量 Intrinsic在广受欢迎的编译器GCC和LLVM中。该API现在已可在GitHub上获得。

RISC-V正继续努力使得RISC-V矢量扩展版本可以获得最终的批准与确立,同时将持续保持更新并且继续增加支持的力度,以创建完整的工具链可使用于整体RISC-V矢量扩展的行业中,从而使得基于RISC-V的矢量处理能够被广泛采用。

已在GCC中实现了大多数API,以验证当前提出的1.0版本规范。除了RISC-V矢量规范外,我们还实现了对标量和矢量FP16(Zfh),原子(Zvamo)和分段加载/存储(Zvlsseg) intrinsic的支持。在汇编器中已实现了当前提出的RVV 1.0版本的完全支持。

团队已经将RISC-V矢量扩展 intrinsics集成到risc-gnu-toolchain中,使用者可以像往常一样通过构建intrinsic来启用GNU工具链。我们还使用内部测试套件并且通过一些内部项目进行了验证。我们已经对intrinsics进行了广泛的测试,并且我们的团队也使用了intrinsics来实现不同的矢量算法来帮助验证RVV矢量的实现。

在不久的将来,RISC-V矢量intrinsics将在LLVM编译器中实现,未来其工作成果亦会提交成为LLVM主体的一部分。同时,在我们将当前的版本更新到最新的RISC-V Vector 1.0草案规范时,我们更欢迎RISC-V社区共同来提供意见和帮助。

SiFive硬件和软件团队的工作重心放在了SiFive Intelligence项目上,SiFive Intelligence是SiFive具有矢量功能的RISC-V核心的处理器IP系列,这些核心的更多详细信息将在年底揭晓。在此之前,我们为构建包含可扩展矢量功能的复合处理器内核的生态所取得的进展感到兴奋,这些生态方面的进展能够帮助设计特定领域计算解决方案,以应对当前特定领域计算所遇到的挑战。

基于开放标准的特定领域架构(DSA)可实现更高层级的性能、能效和垂直集成度,以提供差异化的计算解决方案。SiFive IP业务部门的重点是创建至臻至强的处理器核心架构,使集成在系统中的通用CPU和GPU向前迈进了一大步,从而达到更高的运算水平。整个行业对于RISC-V矢量扩展的兴趣和支持很好地印证了SiFive Intelligence极具潜力。

LLVM的Intrinsics函数及其实现

1 什么是 Intrinsic 函数

Intrinsic 函数是编译器内建的函数,由编译器提供,类似于内联函数。但与内联函数不同的是,因为 Intrinsic 函数是编译器提供,而编译器与硬件架构联系紧密,因此编译器知道如何利用硬件能力以最优的方式实现这些功能。通常函数的代码是 inline 插入,避免函数调用开销。LLVM 支持 Intrinsic 函数的概念。这些函数的名称和语义可以是预先定义,也可以自定义,要求遵守特定的约定。 在有些情况下,可能会调用库函数。例如,在参考文献[2] 中列出的函数,都是调用 libc。总的来说,这些 Intrinsic 函数代表了 LLVM 语言的一种扩展机制,当添加到语言中时,不要求改变 LLVM 的任何转化过程。对其它编译器,Intrinsic 函数也称为内建函数。

在 LLVM 中,Intrinsic 函数一般是在 IR 级代码优化时引入的,也就是由前端产生。

也可以在程序代码中写 Intrinsic 函数,并通过前端直接发射。

这些函数名的前缀一般是保留字 “llvm.”。LLVM 后端选择用最高效的形式将 Intrinsic 函数转换给硬件执行,可以将 Intrinsic 函数拆分为一系列机器指令,也可以映射为单独一条机器指令,并直接调用相应的硬件功能。下文中会针对这两种情况给出实例。

Intrinsic 函数一般是外部函数,开发者不能在自己的代码中实现函数体,而只能调用这些 Intrinsic 函数。获得 Intrinsic 函数的地址是非法的。

2 输出 Intrinsic 函数

以下举例说明 LLVM 如何通过其 Intrinsic 函数优化特定部分代码。

#include<string.h>

int foo(void){

 char str[10] = "str";

 return 0;

}

由 Clang 生产的 LLVM IR 如下:

define i32 @foo() #0 {

entry:

 %str = alloca [10 x i8], align 1

 %0 = bitcast [10 x i8]* %str to i8*

 call void @llvm.memcpy.p0i8.p0i8.i64(i8* %0, i8* getelementptr inbounds ([10 x i8]* @foo.str, i32 0, i32 0), i64 10, i32 1, i1 false)

 ret i32 0

}

其中,llvm.memcpy 就是 clang 输出的 Intrinsic 函数。如果 LLVM 没有定义 llvm.memcpy,相应的内存操作 LLVM IR 代码就应该是一系列 store constant into str[0..3] 内存访问指令,而这些指令通常都是极耗时的。LLVM 后端可将 llvm.memcpy 拆分为一系列高效机器指令,也可以映射为一条特定的机器指令,直接调用硬件的内存操作功能。

再举一例。

int func()

{

 int a[5];

 for (int i = 0; i != 5; ++i)

  a[i] = 0;

 return a[0];

}

使用 Clang 生成未经优化的 IR 代码,其中不包括任何 Intrinsic 函数。

define dso_local i32 @_Z4funcv() #0 {

entry:

 %a = alloca [5 x i32], align 16

 %i = alloca i32, align 4

 store i32 0, i32* %i, align 4

 br label %for.cond

for.cond: ; preds = %for.inc, %entry

 %0 = load i32, i32* %i, align 4

 %cmp = icmp ne i32 %0, 5

 br i1 %cmp, label %for.body, label %for.end

for.body: ; preds = %for.cond

 %1 = load i32, i32* %i, align 4

 %idxprom = sext i32 %1 to i64

 %arrayidx = getelementptr inbounds [5 x i32], [5 x i32]* %a, i64 0, i64 %idxprom

 store i32 0, i32* %arrayidx, align 4

 br label %for.inc

for.inc:  ; preds = %for.body

 %2 = load i32, i32* %i, align 4

 %inc = add nsw i32 %2, 1

 store i32 %inc, i32* %i, align 4

 br label %for.cond

for.end: ; preds = %for.cond

 %arrayidx1 = getelementptr inbounds [5 x i32], [5 x i32]* %a, i64 0, i64 0

 %3 = load i32, i32* %arrayidx1, align 16

 ret i32 %3

}

然后使用 opt 工具对 IR 做 O1 级别优化,得到 IR 如下:

define i32 @_Z4funcv() #0 {

 …

 call void @llvm.memset.p0i8.i64(i8* %a2, i8 0, i64 20, i32 16, i1 false)

其中重要的优化是调用 Intrinsic 函数 llvm.memset.p0i8.i64 为数组填 0。Intrinsic 函数也能用来实现代码的向量化和并行化,从而生成更优化的代码。比如,可以调用 libc 中最优化版本的 memset。

有些 Intrinsic 函数可以重载,比如表示相同操作,但数据类型不同的一族函数。重载通常用来使 Intrinsic 函数可以在任何整数类型上操作。一个或多个参数类型或结果类型可以被重载以接受任何整数类型。

被重载的 Intrinsic 函数名中会包括重载的参数类型,函数名中的每一个参数类型前会有一个句点。只有被重载的类型才会有名称后缀。例如,llvm.ctpop 函数参数是任意宽度的整数,并且返回相同整型宽度的整数。这会引出一族函数,例如 i8 @llvm.ctpop.i8(i8 %val) and i29 @llvm.ctpop.i29(i29 %val). 其中都只有一种类型被重载,函数名中也只有一种类型后缀,如. i8 和. i29。以为参数类型和返回值类型匹配,二者在函数名中共用一个名称后缀。

3 如何定义新 Intrinsic 函数

在使用 LLVM 过程中,开发者也许需要对 LLVM 做定制。这时需要在 LLVM 中添加代码,可能是一个基础类型,可能是一个新 Intrinsic 函数,或者是新的指令。对 LLVM 做扩展需要很大的工作量,涉及更新扩展时要用到的所有 pass。而增加一个 Intrinsic 函数远比增加指令容易,并且对优化 pass 是透明的。如果开发者要增加的功能可以表示成函数调用,Intrinsic 函数是一个不错的可选方法。

要增加 intrinsic 函数,首先要在 LLVM 框架中定义该函数,还有可能要在 clang 中注册该函数,这样前端才能支持在 c 代码中使用这个 intrinsic 函数。这样就可能修改从前端到后端各个不同层次的代码。下例是在自定义后端中实现用自定义 Intrinsic 函数取代 NVVM Intrinsic 函数。

已知有如下 NVVM Intrinsic 函数,这些 Intrinsic 函数是用于支持读 PTX 特殊寄存器:

i32 @llvm.nvvm.read.ptx.sreg.tid.x()

i32 @llvm.nvvm.read.ptx.sreg.tid.y()

i32 @llvm.nvvm.read.ptx.sreg.tid.z()

i32 @llvm.nvvm.read.ptx.sreg.ntid.x()

i32 @llvm.nvvm.read.ptx.sreg.ntid.y()

i32 @llvm.nvvm.read.ptx.sreg.ntid.z()

i32 @llvm.nvvm.read.ptx.sreg.ctaid.x()

i32 @llvm.nvvm.read.ptx.sreg.ctaid.y()

i32 @llvm.nvvm.read.ptx.sreg.ctaid.z()

i32 @llvm.nvvm.read.ptx.sreg.nctaid.x()

i32 @llvm.nvvm.read.ptx.sreg.nctaid.y()

i32 @llvm.nvvm.read.ptx.sreg.nctaid.z()

i32 @llvm.nvvm.read.ptx.sreg.warpsize()

这些 Intrinsic 函数在 .ll 中的调用形式如下:

define <target>_kernel void @nvvm_read_ptx_sreg_tid_x() #0 {

...

 %tid.x = call i32 @llvm.nvvm.read.ptx.sreg.tid.x()

...

 ret void

}

declare i32 @llvm.nvvm.read.ptx.sreg.tid.x()

.ll 文件中的函数可以调用这类 Intrinsic,但要在 .ll 文件中用 declare 声明。这时,Intrinsic 函数的实现可以在另一个 .ll 文件中。或者在某个 lib 中。

如果希望用自定义 Intrinsic 函数取代 NVVM Intrinsic 函数,则需要先定义自定义 Intrinsic 函数。假设希望用 Intrinsic int_<target>_workitem_id 取代 llvm.nvvm.read.ptx.sreg.tid,Intrinsic int_<target>_workitem_id 定义如下:

a. llvm/include/llvm/IR/Intrinsics<target>.td:

如果在 llvm/include/llvm/IR/ 路径下没有与自定义 backend 对应的 Intrinsics<target>.td 文件,可以拷贝已有 backend 的 td 文件,然后在其上修改,这是一个比较快捷的方法。增加对应的 td 文件后,不要忘记在 Intrinsics.td 中包含自定义 backend 的 td 文件,以便框架知道 td 文件的存在。

include "llvm/IR/Intrinsics<target>.td"

在 td 文件中增加自定义 Intrinsic 函数入口,描述 Intrinsic 函数的内存访问优化特性(这控制 Intrinsic 函数是否会被死代码消除、公共子表达式消除等)。任何使用 llvm_any*_ty 类型的 Intrinsic 函数会被 tblgen 认为重载,并在 Intrinsic 函数名中增加后缀。

下例中,Intrinsic<...> 中的内容是对函数签名,描述该 intrinsic 应该如何被调用。签名包括三个部分:返回类型、参数类型和一组标志。这组标志提示了在优化时应该如何处理这个 intrinsic。

class <target>ReadPreloadRegisterIntrinsic

 : Intrinsic<[llvm_i32_ty], [], [IntrNoMem, IntrSpeculatable]>;

multiclass <target>ReadPreloadRegisterIntrinsic_xyz {

 def _x : <target>ReadPreloadRegisterIntrinsic;

 def _y : <target>ReadPreloadRegisterIntrinsic;

 def _z : <target>ReadPreloadRegisterIntrinsic;

}

let TargetPrefix = "<target>" in {

...

defm int_<target>_workitem_id : <target>ReadPreloadRegisterIntrinsic_xyz;

class <target>AtomicIncIntrin : Intrinsic<[llvm_anyint_ty],

 [llvm_anyptr_ty,

 LLVMMatchType<0>,

 llvm_i32_ty, // ordering

 llvm_i32_ty, // scope

 llvm_i1_ty], // isVolatile

 [IntrArgMemOnly, NoCapture<0>], "",

 [SDNPMemOperand]

>;

def int_<target>_atomic_load_add_f32 : <target>AtomicIncIntrin;

LLVM 定义 Intrinsic 函数借用了 GCC builtin,如下例中的 GCCBuiltin<"__nvvm_read_ptx_ sreg_" # regname # "_x">;。

b. llvm/include/llvm/IR/IntrinsicsNVVM.td

// Accessing special registers.

multiclass PTXReadSRegIntrinsic_v4i32<string regname> {

// def _r64 : Intrinsic<[llvm_i128_ty], [], [IntrNoMem]>;

// def _v4i16 : Intrinsic<[llvm_v4i32_ty], [], [IntrNoMem]>;

 def _x : Intrinsic<[llvm_i32_ty], [], [IntrNoMem]>,

 GCCBuiltin<"__nvvm_read_ptx_sreg_" # regname # "_x">;

 def _y : Intrinsic<[llvm_i32_ty], [], [IntrNoMem]>,

 GCCBuiltin<"__nvvm_read_ptx_sreg_" # regname # "_y">;

 def _z : Intrinsic<[llvm_i32_ty], [], [IntrNoMem]>,

 GCCBuiltin<"__nvvm_read_ptx_sreg_" # regname # "_z">;

 def _w : Intrinsic<[llvm_i32_ty], [], [IntrNoMem]>,

 GCCBuiltin<"__nvvm_read_ptx_sreg_" # regname # "_w">;

}

...

defm int_nvvm_read_ptx_sreg_tid : PTXReadSRegIntrinsic_v4i32<"tid">;

def int_nvvm_atomic_load_add_f32 : Intrinsic<[llvm_float_ty],

 [LLVMAnyPointerType<llvm_float_ty>, llvm_float_ty],

  [IntrArgMemOnly, NoCapture<0>]>;

c. 在. ll 文件 llvm/test/CodeGen/<target>/*intrinsics.ll 中增加测试用例:

; Check that nvvm intrinsics are replaced with <target> intrinsics

; RUN: opt -<target>-lower-intrinsics -S < %s | FileCheck %s --check-prefix=CHECK

; CHECK-LABEL: @nvvm_read_ptx_sreg_tid_x

define <target>_kernel void @nvvm_read_ptx_sreg_tid_x() #0 {

; CHECK: @llvm.<target>.workitem.id.x()

 %tid.x = call i32 @llvm.nvvm.read.ptx.sreg.tid.x()

 ret void

}

declare i32 @llvm.nvvm.read.ptx.sreg.tid.x()

d. 在 LowerIntrinsics pass 中替换 intrinsics 函数,实现代码位于文件 llvm/lib/Target/<target>/<target>LowerIntrinsics.cpp 中:

 

bool lowerNVVMIntrinsics(CallInst *CI);

/// This function is used to replace NVVM intrinsics with <TARGET> instrinsics

bool <target>LowerIntrinsics::lowerNVVMIntrinsics(CallInst *CI) {

 IRBuilder<> Builder(CI);

 const Function *Callee = CI->getCalledFunction();

 if (!Callee) {

 return false;

 }

 CallSite CS(CI);

 switch (Callee->getIntrinsicID()) {

 case Intrinsic::nvvm_read_ptx_sreg_tid_x:

 replaceCallWithIntrinsic(Intrinsic::<target>_workitem_id_x, CI, CS.arg_begin(),

 CS.arg_end());

 return true;

 …

 default:

 return false;

 }

}

/// 在此函数中将 NVVM intrinsic 函数转为自定义 intrinsic 函数 :

template <class ArgIt>

static CallInst *replaceCallWithIntrinsic(Intrinsic::ID Intrinsic, CallInst *CI,

  ArgIt ArgBegin, ArgIt ArgEnd) {

 IRBuilder<> Builder(CI->getParent(), CI->getIterator());

 SmallVector<Value *, 8> Args(ArgBegin, ArgEnd);

 CallInst *NewCI = NULL;

 if (Args.empty()) {

 NewCI = Builder.CreateIntrinsic(Intrinsic);

 } else {

 NewCI = Builder.CreateIntrinsic(Intrinsic, Args);

 }

 NewCI->setName(CI->getName());

 if (!CI->use_empty())

 CI->replaceAllUsesWith(NewCI);

 CI->eraseFromParent();

 return NewCI;

}

调用 opt 工具,执行如下命令:

bin/opt -<target>-lower-intrinsics -S ../test/CodeGen/<target>/nvvmintrinsics.ll

输出如下:

; ModuleID = '../test/CodeGen/<target>/nvvmintrinsics.ll'

source_filename = "../test/CodeGen/<target>/nvvmintrinsics.ll"

; Function Attrs: nounwind

define <target>_kernel void @nvvm_read_ptx_sreg_tid_x() #0 {

 %tid.x1 = call i32 @llvm.<target>.workitem.id.x()

 ret void

}

可见,函数体中的 %tid.x = call i32 @llvm.nvvm.read.ptx.sreg.tid.x() 已经替换为 %tid.x1 = call i32 @llvm.<target>.workitem.id.x()。

e. 在 llvm/lib/Target/<target>/<target>ISelLowering.cpp

将 PTX Intrinsic 函数转换成自定义 Intrinsic 函数后,还要实现自定义 Intrinsic 函数的具体功能。在这个例子中,就是要实现 @llvm.<target>.workitem.id.x()。

SDValue <TARGET>TargetLowering::LowerOperation(SDValue Op, SelectionDAG &DAG) {

 case ISD::INTRINSIC_WO_CHAIN:

 return LowerINTRINSIC_WO_CHAIN(Op, DAG);

}

SDValue <TARGET>TargetLowering::LowerINTRINSIC_WO_CHAIN(...){

...

 switch (IntrinsicID) {

...

 case Intrinsic::<target>_workitem_id_x: {

 return loadInputValue(DAG, &<target>::VGPR_32RegClass, MVT::i32,

 SDLoc(DAG.getEntryNode()),

 MFI->getArgInfo().WorkItemIDX);

 }

4 Add relu as LLVM intrinsic

下例说明为了支持 AI 模型中的 relu 激活函数,需要在 LLVM 中做的修改。首先要在 ISA 定义中增加向量 relu 指令,并在编译器中提供相应的 Intrinsic 函数支持。这个例子说明了 Intrinsic 函数重载的用法,实现过程如下:

a. 在 llvm/include/llvm/IR/Intrinsics<target>.td 中添加 Intrinsic 函数定义,其中支持 i32、i16、f32、f16 四种不同数据类型的 relu 操作:

def int_<target>_m_relu_i32 : GCCBuiltin<"__builtin_<target>_m_relu_i32">,

 Intrinsic<[llvm_i32_ty], [llvm_i32_ty], [IntrConvergent]>;

def int_<target>_m_relu_i16 : GCCBuiltin<"__builtin_<target>_m_relu_i16">,

 Intrinsic<[llvm_i16_ty], [llvm_i16_ty], [IntrConvergent]>;

def int_<target>_m_relu_f32 : GCCBuiltin<"__builtin_<target>_m_relu_f32">,

 Intrinsic<[llvm_float_ty], [llvm_float_ty], [IntrConvergent]>;

def int_<target>_m_relu_f16 : GCCBuiltin<"__builtin_<target>_m_relu_f16">,

 Intrinsic<[llvm_half_ty], [llvm_half_ty], [IntrConvergent]>;

b. 在目标平台的指令定义文件 <target>Instruction.td 中增加向量 relu 指令定义:

let hasSideEffects = 1, mayStore = 1, mayLoad = 1 in {

def V_RELU_I32 : MLOP1p_i32<"v_relu_i32",

 [(set i32:$vdst, (int_<target>_m_relu_i32 i32:$src0))]

>;

def V_RELU_I16 :MLOP1p_i16<"v_relu_i16",

 [(set i16:$vdst, (int_<target>_m_relu_i16 i16:$src0))]

>;

def V_RELU_F32 : MLOP1p_f32<"v_relu_f32",

 [(set f32:$vdst, (int_<target>_m_relu_f32 f32:$src0))]

>;

def V_RELU_F16 : MLOP1p_f16<"v_relu_f16",

 [(set f16:$vdst, (int_<target>_m_relu_f16 f16:$src0))]

>;

}

与 Intrinsic 函数定义相应,ISA 指令定义也支持 i32、i16、f32、f16 四种不同数据类型的 relu 操作。

c. 在测试用例 test/Codegen/<target>/relu.ll 中实现调用 relu Intrinsic 函数的代码(仅以 i32 数据类型为例):

declare i32 @llvm.<target>.m.relu.i32(i32)

define void @test(i32 addrspace(1)* %out, i32 %in) {

 %res = call i32 @llvm.<target>.m.relu.i32(i32 %in)

 store i32 %res, i32 addrspace(1)* %out, align 4

 ret void

}

参考资料

https://mp.weixin.qq.com/s/6H3Gltil4VgYEjm1Cbbtfw

LLVM的Intrinsics函数及其实现 - 知乎 (zhihu.com): https://zhuanlan.zhihu.com/p/53659330

https://llvm.org/docs/ExtendingLLVM.html#intrinsic-function

https://mp.weixin.qq.com/s/5LW3TQFsEEnWiGF5lXZV5A

posted @ 2023-02-07 04:16  吴建明wujianming  阅读(1297)  评论(0编辑  收藏  举报