先看什么是LICM,即训练不变代码外提,这是编译优化对循环优化的常用手段。

参考课件:

[http://www.cs.toronto.edu/~pekhimenko/courses/cscd70-w18/docs/Lecture%205%20[LICM%20and%20Strength%20Reduction]%2002.08.2018.pdf](http://www.cs.toronto.edu/~pekhimenko/courses/cscd70-w18/docs/Lecture 5 [LICM and Strength Reduction] 02.08.2018.pdf)

基本上分2个阶段:

  • 找到循环

  • 识别不变式,并把代码外提

对xla来说,只关注第2点,课件中对Loop invariant的定义

  • operands are defined outside loop or invariant themselves

  • not all loop invariant instructions can be moved to preheader

也就是说,判断循环不变式主要看operands,如果本身是常量或定义在循环外那就可以认为是invariant,同时需要主要,不是所有的loop invariant都可以提出去,其实也就是最终是否能做code motion还是取决于是否起到正面的效果并且不影响程序正确执行。

tensorflow xla里的代码在这里

[TF:XLA] Add while loop invariant code motion · tensorflow/tensorflow@51895fe

首先这个功能是整个优化pipeline中的一个pass,所以继承了HloModulePass并实现Run方法。

这个Run方法是所有pass必须实现的接口:

StatusOr Run(HloModule* module) override;

返回值表示是否对module做了修改。

针对WhileLoopInvariantCodeMotion的Run具体又做了2件事:找到while指令,提取while指令中的循环不变式。最终调用的是TryHoistingInvariantInstructionsFromWhileBody:

这个函数的大概实现如下:

  • 当前这个pass只处理参数是元组的情况,对于只有真·一个参数的情况最好用单独的pass处理(我理解就是实现更简单)

  • 如果循环本身只执行一次,跳出当前pass

  • 找到所有循环不变式,并放入一个待提升表

  • 对所有指令进行排序,保证操作数一定出现在操作符的前面

  • 如果循环包含kDomain指令,跳出当前pass

  • 对while循环体内的指令逐个判断:有副作用,是参数,控制结构都不做优化

  • 当一个指令的所有操作数都是不变式或常量的时候,这个指令也是不变式

  • 对于一些特定的指令,单独外提没有意义,先加入待提升表

  • 收集可以提升的指令

  • 指令提升

其中黑体的3处具体逻辑如下:

  • 找到所有循环不变式
/*static*/ std::vector WhileUtil::GetInvariantGTEsForWhileBody(
    const HloComputation& while_body) {
  std::vector result;
  const HloInstruction::InstructionVector root_operands =
      while_body.root_instruction()->operands();
  for (int i = 0; i < root_operands.size(); i++) {
    HloInstruction* instr = root_operands[i];
    if (instr->opcode() == HloOpcode::kGetTupleElement &&
        instr->tuple_index() == i &&
        instr->operand(0) == while_body.parameter_instruction(0)) {
      result.push_back(instr);
    }
  }
  return result;
}

判断root_instruction的operands是否不变量,需要满足3个条件

  • 特定指令不单独外提
bool WhileLoopInvariantCodeMotion::NotWorthHoistingIndividually(
    const HloInstruction& instruction) {
  switch (instruction.opcode()) {
    default:
      return false;
    // 注意这里有个kConstant
    case HloOpcode::kConstant:
      return !hoist_constants_;

    case HloOpcode::kBitcast:
    case HloOpcode::kBroadcast:
    case HloOpcode::kIota:
    case HloOpcode::kReshape:
    case HloOpcode::kReverse:
    case HloOpcode::kSlice:
    case HloOpcode::kTranspose:
    case HloOpcode::kTuple:
      return true;
  }
}
  • 指令外提

指令外提在xla中的实际做法是把这些不变式附加到while指令的参数后,作为多余的参数处理。大致分为3步:1. 生成新的while指令(删除原来的while指令);2. 把不变式加到新while指令的参数末尾;3. 删除不变式指令

看源码的时候有些通用函数的作用如下:

FindOrDie:容器内必存在的时候使用,防止不存在的时候程序出错,因为如果不存在执行这个函数程序直接崩溃
ReplaceInstruction: HloComputation的一个方法,删除旧的指令,添加新的指令
RemoveInstruction: HloComputation的方法,删除指令
AddInstruction: HloComputation的方法,添加指令
ReplaceAllUsesWith: HloInstruction的方法,把调用指令的use换成新的指令,
                      也就是更新使用了原指令的指令的内部引用
parameter_instruction(0): HloComputation的方法,获取computation的输入参数(指令),很多时候是个元组
CreateGetTupleElement: HloInstruction的方法,创建GetTupleElement指令,GetTupleElement和Tuple指令是一对互操作
AppendSuffix:元组后添加元素
ExtractPrefix:截取元组的前n个元素

所谓的优化,就是对这些instruction和computation按照某种优化算法进行操作变换,读源码的时候核心需要注意的也就是3点:

  • 优化算法

  • 指令

  • 指令支持的操作

从头来看,首先就是指令:

https://www.tensorflow.org/xla/operation_semantics#while

其次是指令的操作:分别定义在hlo_instruction.h和hlo_computation.h中,最后是优化算法,这块可以从LLVM的pass中借鉴和学习,同时结合具体场景做等价的实现。


original refer