Home LLVM TableGen
Post
Cancel

LLVM TableGen

TableGen 是一种描述性的语言,用来自动生成 huge include files with tables.td 也可以理解成 target description

目前在 MLIR 中使用 TableGen 的场景主要有注册 DialectOperationPass,并生成对应的 .inc 文件。

现在有个工作需要为 RISCV 扩展新的 Intrinsic,所以深入学习下 TableGen 的使用。

相关文件 llvm/include/llvm/IR/IntrinsicsRISCV.td llvm/lib/Target/RISCV/RISCVInstrInfo.td

在LLVM后端中,TableGen用于描述后端相关的信息,如指令编码,寄存器配置,指令调度,指令选择等。

后端相关的TableGen基类定义在 include/LLVM/Target 下的 td 文件里,其中Target.td定义了机器目标的基本信息基类(Instruction, Register等),和指令选择相关的基类定义在 TargetSelectionDAG.td 中。

llvm/include/llvm/IR/ 下的 IntrinsicsXXX.td 一般是定义对应后端的 Intrinsics,例如 IntrinsicsRISCV.td 定义了 RISCV 相关的 Intrinsics

环境准备

先按照网上各种教程中编译的方法编译一下,主要注意以下 cmake 参数

  • -DLLVM_TARGETS_TO_BUILD : 这里记得加上 RISCV
  • DLLVM_ENABLE_PROJECTS : 我多加了点 “clang;mlir;compiler-rt”
  • DLLVM_CCACHE_BUILD : ON
  • DCMAKE_EXPORT_COMPILE_COMMANDS : ON

编译完成后可以在 build 目录下找到 .td 文件生成的东西,例如 llvm/include/llvm/IR/IntrinsicsRISCV.td 生成了 build/include/llvm/IR/IntrinsicsRISCV.h (在对应的 llvm/include/llvm/IR/CMakeLists.txt 中有描述关系)。

设置 DCMAKE_EXPORT_COMPILE_COMMANDS=ON 后会在 build 目录下生成的compile_commands.json ,复制到 llvm-project 目录下(mv build/compile_commands.json ./),然后配置vscode的clangd插件,方便索引文件:

ctrl + p 输入 clangd,先点击 下载language server;然后 加 settings.json , ctrl + p → ‘> 打开工作区设置json’

1
2
3
4
5
6
7
{
    "clangd.arguments": [
        "--header-insertion=never",
        "--compile-commands-dir=${workspaceFolder}/",
        "--query-driver=**",
    ]
}

TableGen基本语法

llvm/include/llvm/IR/IntrinsicsRISCV.td 文件为例

let

let 是赋值操作符

1
2
let VLOperand = 1;
let TargetPrefix = "riscv";

如果很多对象都有同一个赋值行为,可以加一个 in 来表示作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let TargetPrefix = "riscv" in {

  class BitManipGPRIntrinsics
      : DefaultAttrsIntrinsic<[llvm_any_ty],
                              [LLVMMatchType<0>],
                              [IntrNoMem, IntrSpeculatable]>;

  // Zbb
  def int_xxx_orc_b : BitManipGPRIntrinsics;

  // Zbc or Zbkc
  def int_xxx_clmul  : BitManipGPRGPRIntrinsics;
  def int_xxx_clmulh : BitManipGPRGPRIntrinsics;

  // Zbc
  def int_xxx_clmulr : BitManipGPRGPRIntrinsics;

  // Zbkx
  def int_xxx_xperm4  : BitManipGPRGPRIntrinsics;
  def int_xxx_xperm8  : BitManipGPRGPRIntrinsics;
} // TargetPrefix = "riscv"

类型

type

1
2
3
4
5
6
7
8
9
10
11
12
// llvm/lib/Target/RISCV/RISCVInstrInfo.td
class ImmAsmOperand<string prefix, int width, string suffix> : AsmOperandClass {
  let Name = prefix # "Imm" # width # suffix;
  let RenderMethod = "addImmOperands";
  let DiagnosticType = !strconcat("Invalid", Name);
}

class SImmAsmOperand<int width, string suffix = "">
    : ImmAsmOperand<"S", width, suffix> {
}
// 传入整数 13 和 字符串 "Lsb0"
let ParserMatchClass = SImmAsmOperand<13, "Lsb0">;
  • string

字符串,两侧加双引号

let RenderMethod = "addImmOperands";

  • int

整数

  • bits

bits是带位宽限制的整数,用于表示指令编码,立即数编码等限长整数。

bits<13> imm=12312;

如果 imm 超过了 13 位,会截断超出位宽的部分。

  • code

嵌入一些 c++ 代码

1
2
3
def uimm6gt32 : ImmLeaf<XLenVT, [{
  return isUInt<6>(Imm) && Imm > 32;
}]>;
  • list

list<T> 表示一个 T 类型的列表,例如 list<Register>

class

c++ 类似, .td 中的 class 也可以被实例化和继承。

1
2
3
4
5
6
7
// BitManipGPRIntrinsics 继承自 DefaultAttrsIntrinsic
class BitManipGPRIntrinsics
    : DefaultAttrsIntrinsic<[llvm_any_ty],
                            [LLVMMatchType<0>],
                            [IntrNoMem, IntrSpeculatable]>;
// int_xxx_orc_b 是 BitManipGPRIntrinsics 的实例
def int_xxx_orc_b : BitManipGPRIntrinsics;
1
2
3
4
5
6
class persionInBJ<string name_, int age_> : Location<"BJ"> {
  string name = name_;
  int age = age_;
  int height = 180;
  bit gender;
}

def

def 用于定义一个记录。一个记录可以被看做是有名字,有类型,具有特定属性的结构体。每个记录的名字是唯一的。

1
2
3
4
def rec{
 int a=1;
 string b="tbl is fun";
}

一个 class 可以看为是一个抽象的记录,def 可以看为是一个具体的记录。所以 def 实例化 class

1
2
3
4
5
6
7
8
class MyInstr{
 string asmname;
 bits<32> encoding;
}
def ADD: MyInstr{
 let asmname="add";
 let encoding{31-26}=1;
}

注意 Intrinsics 相关的名称需要以 int_ 开头。

1
2
3
4
5
6
7
8
9
10
11
// 定义
// llvm/include/llvm/IR/IntrinsicsRISCV.td
class BitManipGPRIntrinsics;
def int_xxx_orc_b : BitManipGPRIntrinsics;

// 编译后生成的代码
// build/include/llvm/IR/IntrinsicsRISCV.h
namespace Intrinsic {
enum RISCVIntrinsics : unsigned {
  ...
  xxx_orc_b, // llvm.riscv.orc.b

multiclass 和 defm

如果指令根据操作数类型不同,下降时有不同的处理行为,例如一般i32和i64的指令处理可能不同,就需要定义很多相似的指令。

TableGen提供了multiclassdefm让我们可以一次性定义多条相似指令。

multiclass只是TableGen提供的一种创建多个记录的快捷方式,它本身并不是类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// llvm/include/llvm/IR/IntrinsicsRISCV.td
class MaskedAtomicRMWFourArg<LLVMType itype>
    : Intrinsic<[itype], [llvm_anyptr_ty, itype, itype, itype],
                [IntrArgMemOnly, NoCapture<ArgIndex<0>>, ImmArg<ArgIndex<3>>]>;

// We define 32-bit and 64-bit variants of the above, where T stands for i32
// or i64 respectively:
multiclass MaskedAtomicRMWFourArgIntrinsics {
  // i32 @llvm.<name>.i32.<p>(any*, i32, i32, i32 imm);
  def _i32 : MaskedAtomicRMWFourArg<llvm_i32_ty>;
  // i64 @llvm.<name>.i32.<p>(any*, i64, i64, i64 imm);
  def _i64 : MaskedAtomicRMWFourArg<llvm_i64_ty>;
}

// @llvm.riscv.masked.atomicrmw.*.{i32,i64}.<p>(
//   ptr addr, ixlen oparg, ixlen mask, ixlenimm ordering)
defm int_xxx_masked_atomicrmw_add : MaskedAtomicRMWFourArgIntrinsics;
defm int_xxx_masked_atomicrmw_sub : MaskedAtomicRMWFourArgIntrinsics;

其中:

  • multiclass : 定义具有两种数据类型的atomic指令
  • defm : 实例化 multiclass 中所有的记录,每个记录名称为 defm 的名字和 multiclass 中的 def 的名字相拼接,如

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    // build/include/llvm/IR/IntrinsicsRISCV.h
    namespace Intrinsic {
    enum RISCVIntrinsics : unsigned {
      ...
      xxx_masked_atomicrmw_add_i32, // llvm.riscv.masked.atomicrmw.add.i32
      xxx_masked_atomicrmw_add_i64, // llvm.riscv.masked.atomicrmw.add.i64
      ...
    
    // 下降代码中直接使用 `Intrinsic::xxx_masked_atomicrmw_add_i32` 对象
    

defvar

defvar 用于定义一个变量。

  • 不直接定义域
  • 同一作用域内不能重复定义
  • 必须有初值,自动推导类型

foreach

foreach 用于遍历一个列表,作用在 in 后的语句块内,生成多个记录。

1
2
3
4
5
6
7
8
9
// llvm/include/llvm/IR/IntrinsicsRISCVXsf.td
multiclass RISCVSFCustomVC_XV<list<string> type> {
  foreach t = type in {
    defvar ImmScalar = !eq(t, "i");
    def "int_xxx_sf_vc_" # t # "v_se"   : RISCVSFCustomVC_XV<HasDst=0, HasSE=1, ImmScalar=ImmScalar>;
    def "int_xxx_sf_vc_v_" # t # "v_se" : RISCVSFCustomVC_XV<HasDst=1, HasSE=1, ImmScalar=ImmScalar>;
    def "int_xxx_sf_vc_v_" # t # "v"    : RISCVSFCustomVC_XV<HasDst=1, HasSE=0, ImmScalar=ImmScalar>;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
multiclass GradeRecords<string name, list<int> grades> {
  def NAME#_C: Person<name>, Grade<grades[0]>;
  def NAME#_M: Person<name>, Grade<grades[1]>;
  def NAME#_E: Person<name>, Grade<grades[2]>;
}

-> 等价于

multiclass GradeRecords<string name, list<int> grades> {
  defvar clses = ["C", "M", "E"];
  foreach i = 0..2 in {
    def _#clses[i]: Person<name>, Grade<grades[i]>;
  }
}

if

根据 value 判断选择执行的语句块

1
2
3
4
5
if !lt(grades[i], 60) then {
  def _#clses[i]: Person<name>, Grade<grades[i]> {string level = "fail"; };
} else {
  def _#clses[i]: Person<name>, Grade<grades[i]>;
}

实例

参数类型

常见的参数类型有:

  • 整数
    • llvm_i8_ty, llvm_i16_ty, llvm_i32_ty, llvm_i64_ty
    • llvm_anyint_ty : 任意整数
    • llvm_i32_ty 一般可以用来表示 immInt32Regs
  • 浮点
    • llvm_half_ty, llvm_float_ty, llvm_bfloat_ty
  • 地址
    • llvm_ptr_ty : 任意指针

例如:一个BinaryOp的Intrinsic,可以定义为

1
2
3
4
  class XXXIntrinsic<string name>
    : Intrinsic<[], [llvm_ptr_ty, llvm_ptr_ty, llvm_ptr_ty, llvm_i32_ty],
                []>,
      ClangBuiltin<"__XXX_" # name>;

其中 [llvm_ptr_ty, llvm_ptr_ty, llvm_ptr_ty, llvm_i32_ty] 分别表示 addr $dst,addr $lsh, addr $rsh, Int32Regs $size

  • 读写属性
    • IntrReadMem, IntrWriteMem, IntrNoMem

multiclass + foreach + defm

1
2
3
4
5
6
7
8
9
10
11
12
multiclass XXXBinaryOp {
  defvar DataTypes = ["u8", "s8", "u16", "s16", "u32", "s32", "f16", "f32", "bf16"];
  defvar LLVMTypes = [llvm_i8_ty, llvm_i8_ty, llvm_i16_ty, llvm_i16_ty, llvm_i32_ty, llvm_i32_ty, llvm_half_ty, llvm_float_ty, llvm_bfloat_ty];
  foreach i = 0...8 in {
    // Scalar operations
    def "int_xxx_scalar_binary_" # NAME # _#DataTypes[i] : ScalarBinary<NAME # _#DataTypes[i], LLVMTypes[i]>;
    // Vector operations
    def "int_xxx_vector_binary_" # NAME # _#DataTypes[i] : VectorBinary<NAME # _#DataTypes[i]>;
  }
}

defm add : XXXBinaryOp;

验证

验证定义的op接口主要有两个方法

  1. 使用 llvm-tblgen 生成编译 顶层td ,查看生成的op是否符合预期
1
./build/bin/llvm-tblgen llvm/include/llvm/IR/Intrinsics.td -I llvm/include/llvm -I llvm/include/llvm/IR -I build/include/ -I llvm/include/ -o log
  1. 使用 opt 测试接口

ll 文件中调用写好的接口,如果下面的指令报错,证明指令正确定义,且格式符合需求

1
./build/bin/opt llvm/test/CodeGen/RISCV/RISCVEXT/memcpy.ll -o tmp.o

参考

This post is licensed under CC BY-NC-SA 4.0 by the author.

AI Compiler 中的计算图切分

Triton Example