tensorflow xla 符号执行
tensorflow xla里的tf2xla部分是通过符号执行来完成的。
什么是符号执行
符号执行是一种程序分析技术,使用符号执行分析一个程序时,该程序会使用符号值作为输入,而非一般执行程序时使用的具体值。
比如下面的程序(来自wiki)
int f() {
...
y = read();
z = y * 2;
if (z == 12) {
fail();
} else {
printf("OK");
}
}
一般我们执行这个程序,会读取一个y的值,比如 y = 3,那么z=6,此时程序进入fail分支,如果y=6,则最终打印OK,每次程序运行都会读取一个具体的数值,最终也会得到一个具体的结果。但对于符号执行来说,我们会给y一个值,比如y = λ,其中λ只是代表输入的一个符号,所以到了if判断的地方,没法具体得到这个值是true还是false,执行路径就会分叉,最终形成一颗树的结构。当程序最终执行完,从这个生成树的叶子节点到跟节点就会有很多路径,利用这些路径信息就可以对程序分析做很多事情。
参考:
https://en.wikipedia.org/wiki/Symbolic_execution
tensorflow中的符号执行
从定义可以看出,符号执行首先需要一个符号输入,最终得到的也是一个(或多个)带符号的输出,所以这里就有符号值(symbolic value)的概念。这也正好对应xla最终生成的函数的输入和输出,正好对应了tf2xla中CompileGraph的主流程:
BuildArguments 处理输入
ExecuteGraph 符号执行
BuildComputation 处理输出
BuildArguments
xla的参数有几种类型,符号输入主要涉及的kResource和kParameter;主要就是通过 xla::Parameter来构造输入,示例代码:
tuple = xla::Parameter(builder, 0, (*input_shapes)[0], "arg_tuple");
arg_handles[i] = xla::Parameter(builder, i, (*input_shapes)[i], absl::StrCat("arg", i));
ExecuteGraph
这个函数里主要就是调用了GraphCompiler,对图中每个op进行编译(符号执行)。这里的执行分为2步:
创建kernel
编译op(执行):执行的过程中会构造xla的图
据一个 tf.roll的例子,截取这个op在xla中的kernel实现如下:
void Compile(XlaOpKernelContext* ctx) override {
const TensorShape input_shape = ctx->InputShape(0);
xla::XlaOp shift = ctx->Input(1);
const TensorShape shift_shape = ctx->InputShape(1);
const TensorShape axis_shape = ctx->InputShape(2)
xla::Literal axis;
xla::XlaOp output = ctx->Input(0);
xla::PrimitiveType shift_type = ctx->input_xla_type(1);
int64 num_axes = axis_shape.dims() == 0 ? 1 : axis_shape.dim_size(0);
for (int64 i = 0; i != num_axes; ++i) {
auto cur_axis_status = axis_shape.dims() == 0
? axis.GetIntegralAsS64({})
: axis.GetIntegralAsS64({i});
int64 cur_axis = cur_axis_status.ValueOrDie();
xla::XlaOp offset =
shift_shape.dims() == 0
? shift
: xla::Reshape(xla::SliceInDim(shift, /*start_index=*/i,
/*limit_index=*/i + 1,
/*stride=*/1, /*dimno=*/0),
{});
xla::XlaOp axis_size = xla::ConstantR0WithType(
ctx->builder(), shift_type, input_shape.dim_size(cur_axis));
// Adjust large offsets into [0, axis_size). This also makes negative
// offsets positive.
offset = ((offset % axis_size) + axis_size) % axis_size;
// Stack two copies of the dimension, then slice from the calculated
// offset.
xla::XlaOp concat =
xla::ConcatInDim(ctx->builder(), {output, output}, cur_axis);
std::vector start_indices(
input_shape.dims(), xla::Zero(ctx->builder(), shift_type));
start_indices[cur_axis] = axis_size - offset;
output =
xla::DynamicSlice(concat, start_indices, input_shape.dim_sizes());
}
ctx->SetOutput(0, output);
}
参考:
[TF:XLA] Implement support for tf.roll · tensorflow/tensorflow@cc3bd2c
这个op的作用就是对向量或矩阵按某个纬度做平移变换 :
# 't' is [0, 1, 2, 3, 4]
roll(t, shift=2, axis=0) ==> [3, 4, 0, 1, 2]
# shifting along multiple dimensions
# 't' is [[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]]
roll(t, shift=[1, -2], axis=[0, 1]) ==> [[7, 8, 9, 5, 6], [2, 3, 4, 0, 1]]
# shifting along the same axis multiple times
# 't' is [[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]]
roll(t, shift=[2, -3], axis=[1, 1]) ==> [[1, 2, 3, 4, 0], [6, 7, 8, 9, 5]]
参考
https://www.tensorflow.org/api_docs/python/tf/roll
里面最主要的3行代码在这里:
xla::XlaOp concat = xla::ConcatInDim(ctx->builder(), {output, output}, cur_axis);
output = xla::DynamicSlice(concat, start_indices, input_shape.dim_sizes());
ctx->SetOutput(0, output);
前2行是用xla的builder 接口构造xla的图,每个接口代表xla hlo图中的一个op,但这个图的构造就是通过对原图进行执行(编译)得到的,最后把输出作为xla中op的输出。
BuildComputation
包含了对输出节点以及kResource的处理,保证输出节点一定在hlo computation的最后。可以参考源码注释:
// Builds the XLA computation. We *always* form a tuple here to ensure that
// the output value is the last thing added into the XLA computation, even
// if there is only one output value.
Comments