CIRCT 代码分析(WIP)

Keywords: #C++ #LLVM #CIRCT

前言

看了 MLIR 的介绍之后,我认为相较于FIRRTLCIRCT 才是真正未来的开源综合器框架。总结原因有这几点:

  • FIRRTL 主要贡献者 Schuyler Eldridge 在加入 SiFive 后在 Chris Lattner 的组中干活。根据 FIRRTL 以及 CIRCT 的数据统计可知未来的工作重心会转向 CIRCT。
  • FIRRTL 的综合器框架逐步显示出其局限性,因为涉及到语言定义,新功能难以快速添加,需要 MLIR 这种局部 IR Transform 架构,便于 Verification 的优化工作。
  • 基于 C++ 的 CIRCT 远远快于基于 JVM 的FIRRTL。

因此虽然目前已经很熟悉 FIRRTL 了,也对 FIRRTL 做了一些贡献工作,但是还是准备转向 CIRCT,一是为论文工作做准备,二是学习 LLVM 和 C++。 由于笔者对于 LLVM 和 C++ 没有任何任何经验,因此施工会很慢。

环境搭建

在 Scala 的开发中我十分依赖 Intellij IDEA,因此还是使用 CLion 作为 LLVM 相关的主要的开发工具。 我在我的 Build Farm 中有两台高性能主机,由于 LLVM 项目过大,因此使用 distcc 远程编译,由于 distcc 和本文无关,其的配置过程省略。相较于 CIRCT Readme 的配置,对于 CMake 有如下改动:

# 构建 LLVM
cd circt
mkdir -p llvm/build
cd llvm/build
cmake -G Ninja ../llvm \
  -DLLVM_ENABLE_PROJECTS="mlir" \
  # 并不需要 RISCV 的 Target,因此去掉
  -DLLVM_TARGETS_TO_BUILD="X86" \
  -DLLVM_ENABLE_ASSERTIONS=ON \
  # 使用 lld linking 以加速和节省内存
  -DLLVM_USE_LINKER=lld \
  -DCMAKE_BUILD_TYPE=DEBUG \
  # 使用 distcc 构建
  -DCMAKE_C_COMPILER_LAUNCHER=distcc \
  -DCMAKE_CXX_COMPILER_LAUNCHER=distcc
# 使用 distcc pump 远端 preprocessing 及 compiling 
pump --startup
# 并行度 100 构建 LLVM
pump ninja -j 100
# 构建 CIRCT
mkdir build
cd build
# 不使用 distcc 构建
# 1. 项目不大
# 2. CLion 无法支持 distcc pump
cmake -G Ninja .. \
    -DMLIR_DIR=$PWD/../llvm/build/lib/cmake/mlir \
    -DLLVM_DIR=$PWD/../llvm/build/lib/cmake/llvm \
    -DLLVM_USE_LINKER=lld \
    -DLLVM_ENABLE_ASSERTIONS=ON \
    -DCMAKE_BUILD_TYPE=DEBUG \
ninja

在 CLion 中,除了导入 CMakeLists.txt 之外,还需要单独配置 CMake 的相关选项,以对齐命令行和 CLion 的编译选项,共享 Ninja build cache。不想解释图形界面操作,在.idea/workspace.xml添加以下配置即可。

<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="CMakeSettings">
    <configurations>
      <configuration PROFILE_NAME="Debug" CONFIG_NAME="Debug" GENERATION_OPTIONS="-DMLIR_DIR=llvm/build/lib/cmake/mlir -DLLVM_DIR=llvm/build/lib/cmake/llvm -DLLVM_USE_LINKER=lld -DLLVM_ENABLE_ASSERTIONS=ON -G Ninja" GENERATION_DIR="build" ENABLED="true" />
    </configurations>
  </component>
  <component name="ClangdSettings">
    <option name="formatViaClangd" value="true" />
  </component>
</project>

GENERATION_OPTIONS 的作用是让 CIRCT 找到 MLIRLLVM 的构建目录,不同于命令行的 $PWD,直接将项目目录作为根目录即可。GENERATION_DIR 需要设置到 build,以共享 Ninja build cache。

项目结构

目前 CIRCT 处于项目的初期阶段,目录结构还比较初步,因此不做详细赘述。本文将从简单的 Transform 开始,首先深度优先地理一遍 FIRRTL 到 Verilog 的转换过程,然后再广度优先深入 MLIR 理解详细实现。 首先不考虑对 FIRRTL 的 Parse,直接看位于 test/firtool/firtool.mlir 的 FIRRTL MLIR 到 Verilog 转换测试:

firtool test/firtool/firtool.mlir --format=mlir -verilog

// Standard header to adapt well known macros to our needs.
`ifdef RANDOMIZE_GARBAGE_ASSIGN
`define RANDOMIZE
`endif
`ifdef RANDOMIZE_INVALID_ASSIGN
`define RANDOMIZE
`endif
`ifdef RANDOMIZE_REG_INIT
`define RANDOMIZE
`endif
`ifdef RANDOMIZE_MEM_INIT
`define RANDOMIZE
`endif
`ifndef RANDOM
`define RANDOM $random
`endif
// Users can define 'PRINTF_COND' to add an extra gate to prints.
`ifdef PRINTF_COND
`define PRINTF_COND_ (`PRINTF_COND)
`else
`define PRINTF_COND_ 1
`endif
// Users can define 'STOP_COND' to add an extra gate to stop conditions.
`ifdef STOP_COND
`define STOP_COND_ (`STOP_COND)
`else
`define STOP_COND_ 1
`endif

// Users can define INIT_RANDOM as general code that gets injected into the
// initializer block for modules with registers.
`ifndef INIT_RANDOM
`define INIT_RANDOM
`endif

// If using random initialization, you can also define RANDOMIZE_DELAY to
// customize the delay used, otherwise 0.002 is used.
`ifndef RANDOMIZE_DELAY
`define RANDOMIZE_DELAY 0.002
`endif

// Define INIT_RANDOM_PROLOG_ for use in our modules below.
`ifdef RANDOMIZE
  `ifndef VERILATOR
    `define INIT_RANDOM_PROLOG_ `INIT_RANDOM #`RANDOMIZE_DELAY begin end
  `else
    `define INIT_RANDOM_PROLOG_ `INIT_RANDOM
  `endif
`else
  `define INIT_RANDOM_PROLOG_
`endif
module Top(
  input  [7:0] in,
  output [7:0] out);

  assign out = in;  // test/firtool/firtool.mlir:7:5
endmodule

在 CLion 右上方的 Build 中直接添加一个 CMake Application 即可(这也是便于Trace Debug)

找到函数入口

tools/firtool/firtool.cpp 中的 main 是程序入口:

int main(int argc, char **argv) {
  InitLLVM y(argc, argv);

  // Register any pass manager command line options.
  registerMLIRContextCLOptions();
  registerPassManagerCLOptions();

  // Parse pass names in main to ensure static initialization completed.
  cl::ParseCommandLineOptions(argc, argv, "circt modular optimizer driver\n");

  // Figure out the input format if unspecified.
  if (inputFormat == InputUnspecified) {
    if (StringRef(inputFilename).endswith(".fir"))
      inputFormat = InputFIRFile;
    else if (StringRef(inputFilename).endswith(".mlir"))
      inputFormat = InputMLIRFile;
    else {
      llvm::errs() << "unknown input format: "
                      "specify with -format=fir or -format=mlir\n";
      exit(1);
    }
  }

  // Set up the input file.
  std::string errorMessage;
  auto input = openInputFile(inputFilename, &errorMessage);
  if (!input) {
    llvm::errs() << errorMessage << "\n";
    return 1;
  }

  auto output = openOutputFile(outputFilename, &errorMessage);
  if (!output) {
    llvm::errs() << errorMessage << "\n";
    return 1;
  }

  if (failed(processBuffer(std::move(input), output->os())))
    return 1;

  output->keep();
  return 0;
}

下面逐行解释:

  InitLLVM y(argc, argv);

构建 LLVM 的运行时环境,在 LLVM 文档中可知,其主要工作是构建 StackTrace 和 Error handler,不赘述。

  // Register any pass manager command line options.
  registerMLIRContextCLOptions();
  registerPassManagerCLOptions();

  // Parse pass names in main to ensure static initialization completed.
  cl::ParseCommandLineOptions(argc, argv, "circt modular optimizer driver\n");

注册 CLI 选项,MLIR相关,不赘述。

/// Allow the user to specify the input file format.  This can be used to
/// override the input, and can be used to specify ambiguous cases like standard
/// input.
enum InputFormatKind { InputUnspecified, InputFIRFile, InputMLIRFile };

static cl::opt<InputFormatKind> inputFormat(
    "format", cl::desc("Specify input file format:"),
    cl::values(clEnumValN(InputUnspecified, "autodetect",
                          "Autodetect input format"),
               clEnumValN(InputFIRFile, "fir", "Parse as .fir file"),
               clEnumValN(InputMLIRFile, "mlir", "Parse as .mlir file")),
    cl::init(InputUnspecified));
...
  if (inputFormat == InputUnspecified) {
    if (StringRef(inputFilename).endswith(".fir"))
      inputFormat = InputFIRFile;
    else if (StringRef(inputFilename).endswith(".mlir"))
      inputFormat = InputMLIRFile;
    else {
      llvm::errs() << "unknown input format: "
                      "specify with -format=fir or -format=mlir\n";
      exit(1);
    }
  }

推断 inputFormat,不赘述。

  // Set up the input file.
  std::string errorMessage;
  auto input = openInputFile(inputFilename, &errorMessage);
  if (!input) {
    llvm::errs() << errorMessage << "\n";
    return 1;
  }

  auto output = openOutputFile(outputFilename, &errorMessage);
  if (!output) {
    llvm::errs() << errorMessage << "\n";
    return 1;
  }

控制输入输出,不赘述。

  if (failed(processBuffer(std::move(input), output->os())))
    return 1;

找到业务逻辑入口,processBuffer

基于 MLIR 的主要业务逻辑

/// Process a single buffer of the input.
static LogicalResult
processBuffer(std::unique_ptr<llvm::MemoryBuffer> ownedBuffer,
              raw_ostream &os) {
  MLIRContext context;

  // Register our dialects.
  context.loadDialect<firrtl::FIRRTLDialect, rtl::RTLDialect, comb::CombDialect,
                      sv::SVDialect>();

  llvm::SourceMgr sourceMgr;
  sourceMgr.AddNewSourceBuffer(std::move(ownedBuffer), llvm::SMLoc());
  SourceMgrDiagnosticHandler sourceMgrHandler(sourceMgr, &context);

  // Nothing in the parser is threaded.  Disable synchronization overhead.
  context.disableMultithreading();

  // Apply any pass manager command line options.
  PassManager pm(&context);
  pm.enableVerifier(verifyPasses);
  applyPassManagerCLOptions(pm);

  OwningModuleRef module;
  if (inputFormat == InputFIRFile) {
    firrtl::FIRParserOptions options;
    options.ignoreInfoLocators = ignoreFIRLocations;
    module = importFIRRTL(sourceMgr, &context, options);

    // If we parsed a FIRRTL file and have optimizations enabled, clean it up.
    if (!disableOptimization) {
      pm.addPass(createCSEPass());
      pm.addPass(createCanonicalizerPass());
    }
  } else {
    assert(inputFormat == InputMLIRFile);
    module = parseSourceFile(sourceMgr, &context);
  }
  if (!module)
    return failure();

  // Allow optimizations to run multithreaded.
  context.disableMultithreading(false);

  // Run the lower-to-rtl pass if requested.
  if (lowerToRTL) {
    if (enableLowerTypes)
      pm.nest<firrtl::CircuitOp>().addNestedPass<firrtl::FModuleOp>(
          firrtl::createLowerFIRRTLTypesPass());
    pm.addPass(createLowerFIRRTLToRTLModulePass());
    pm.addNestedPass<rtl::RTLModuleOp>(createLowerFIRRTLToRTLPass());

    // If enabled, run the optimizer.
    if (!disableOptimization) {
      pm.addNestedPass<rtl::RTLModuleOp>(sv::createRTLCleanupPass());
      pm.addPass(createCSEPass());
      pm.addPass(createCanonicalizerPass());
    }
  }

  if (failed(pm.run(module.get())))
    return failure();

  // Finally, emit the output.
  switch (outputFormat) {
  case OutputMLIR:
    module->print(os);
    return success();
  case OutputDisabled:
    return success();
  case OutputVerilog:
    if (lowerToRTL)
      return exportVerilog(module.get(), os);
    return exportFIRRTLToVerilog(module.get(), os);
  }
};

开始逐行分析:

  MLIRContext context;

构建 MLIRContext,未来做解释。

  // Register our dialects.
  context.loadDialect<firrtl::FIRRTLDialect, rtl::RTLDialect, comb::CombDialect,
                      sv::SVDialect>();

注册 Dialect 到 context 中。

  llvm::SourceMgr sourceMgr;
  sourceMgr.AddNewSourceBuffer(std::move(ownedBuffer), llvm::SMLoc());
  SourceMgrDiagnosticHandler sourceMgrHandler(sourceMgr, &context);

构建SourceMgr,未来做解释。

  // Nothing in the parser is threaded.  Disable synchronization overhead.
  context.disableMultithreading();

禁止多线程,未来做解释。但是这儿有点意思,需要显式禁止多线程,说明 MLIR 是一个激进的多线程编译框架,和 FIRRTL 相比的,其优势就逐步体现出来了。对于多 Module 的并行优化一定会显著强于 FIRRTL 的。

  // Apply any pass manager command line options.
  PassManager pm(&context);
  pm.enableVerifier(verifyPasses);
  applyPassManagerCLOptions(pm);

构建 PassManager,提供 Pass 的注册接口,所有的 Pass 通过 pm.addPasspm.addNestedPass<T> 的方式注册到 PassManager 之中。 todo addNestedPassaddPass 差别在 MLIR 特有的 Nested 语法。

  if (failed(pm.run(module.get())))
    return failure();

这是真正的 Transform 入口。

从FIRRTL Parser开始讲起

这一章段落主要膜 Chris 的 FIRRTL parser。

FIRRTL Parser 的主要功能是 parse FIRRTL 到 MLIR。

类的声明在:lib/Dialect/FIRRTL/Import/FIRParser.cpp

  FIRParser(GlobalFIRParserState &state) : state(state) {}
private:
  FIRParser(const FIRParser &) = delete;
  void operator=(const FIRParser &) = delete;

类的初始化只有通过传入一个类型为 GlobalFIRParserState 指针实现,形如 FIRParser xxx; 的构造会直接爆炸。 传入的GlobalFIRParserState可以理解成为一个全局的单例,看起来它会构造一个单例 Lexer,因此看起来这个结构下 CIRCT 只能 Parse 一个 FIRRTL 文件?

struct GlobalFIRParserState {
  GlobalFIRParserState(const llvm::SourceMgr &sourceMgr, MLIRContext *context,
                       FIRParserOptions options)
      : context(context), options(options), lex(sourceMgr, context),
        curToken(lex.lexToken()){};
}

直接DFS到 Lexer:

class FIRLexer {
  ???
}

看起来 LLVM 并没有 General 的 Lexer,Chris 直接手撸出来了。(膜一把) 然后 DFS 到 Token,五体投地:

class FIRToken {
public:
  enum Kind {
#define TOK_MARKER(NAME) NAME,
#define TOK_IDENTIFIER(NAME) NAME,
#define TOK_LITERAL(NAME) NAME,
#define TOK_PUNCTUATION(NAME, SPELLING) NAME,
#define TOK_KEYWORD(SPELLING) kw_##SPELLING,
#define TOK_LPKEYWORD(SPELLING) lp_##SPELLING,
#include "FIRTokenKinds.def"
  };

我已经不知道说什么好了,对比 Scala 使用 Antlr4 暴力 Parser,Chris 其实纯粹的手写了一把 C++ FIRRTL Parser。 这个地方又感慨一下 CPP 的 X Macro。用的真的很漂亮。

Parser 的入口在 circt::firrtl::importFIRRTL

OwningModuleRef circt::firrtl::importFIRRTL(SourceMgr &sourceMgr, MLIRContext *context, FIRParserOptions options)

首先构造 Top 模块:

  // This is the result module we are parsing into.
  OwningModuleRef module(ModuleOp::create(
      FileLineColLoc::get(sourceBuf->getBufferIdentifier(), /*line=*/0,
                          /*column=*/0, context)));

然后通过 parseCircuit递归构造 Module

ParseResult FIRCircuitParser::parseCircuit() {
  OpBuilder b(mlirModule.getBodyRegion());
  auto circuit = b.create<CircuitOp>(info.getLoc(), name); // 在 MLIR 中创建 Circuit
  FIRModuleParser mp(getState(), circuit);
  if (getToken().is(FIRToken::kw_module) ? mp.parseModule(moduleIndent) : mp.parseExtModule(moduleIndent))
}

mp.parseExtModule 为例,circuit 上面创建的 circuit operator,在 MLIR 中没有 FIRRTL 的 Indent 的概念,直接在对应的 circuit.getBodyBuilder 中创建对应的模块。这也是 MLIR 应用其 nested pass 的地方。

auto builder = circuit.getBodyBuilder();
auto fmodule = builder.create<FExtModuleOp>(info.getLoc(), name, portList, defName);

使用 tablegen 对 MLIR 的进行声明

include/circt/Dialect/FIRRTL 下的 td 文件都是 FIRRTL 的 MLIR 定义,之后将会对 tablegen 进行详细解释。

comments powered by Disqus