TIK表达式
什么是TIK表达式
本节主要介绍TIK中表达式即Expr的概念和相关技巧。
表达式(expression)是值、TIK变量(Tensor、Scalar、InputScalar)和运算符的组合。 值自身也被认为是一个表达式,变量也是。例如:
# 变量 a # 立即数 1 # 计算式 b * 3 + 10 # 逻辑式 e <= f # 复杂式 # 如for_range里的循环变量i,j等 (i * 256) + (j * 16) + 8
在TIK前端表达中,变量(Variable),立即数(Int、Float、String),式子中包含逻辑操作符(And、Or、Not、Add、Sub、Mul、Div、Mod、FloorDiv、FloorMod、Min、Max、EQ、NE、LT、LE、GT、GE)等都可以视为是表达式Expr。
但是读者在写TIK的前端表达的时候,有些细节还是需要注意,下面对一些使用要点进行介绍。
分例1
a_scalar = tik_instance.Scalar("int32", "a_scalar", init_value=0) b_scalar = tik_instance.Scalar("int32", "b_scalar", init_value=0) c_scalar = a_scalar + b_scalar >>> print(type(a_scalar)) <class 'tbe.tik.api.tik_scalar.Scalar'> >>> print(type(c_scalar)) <class 'tbe.tik.tik_lib.tik_expr.Expr'>
这里定义了两个Scalar变量:a_scalar和b_scalar,然后将a_scalar和b_scalar相加,想得到c_scalar,我们通过打印其类型,会发现两个Scalar类型的变量,加起来的类型是Expr类型,并不是Scalar类型。
如果例子这么写:
分例2
a_scalar = tik_instance.Scalar("int32", "a_scalar", init_value=0) b_scalar = tik_instance.Scalar("int32", "b_scalar", init_value=0) c_scalar = tik_instance.Scalar("int32", "c_scalar", init_value=a_scalar+b_scalar) >>> print(type(a_scalar)) <class 'tbe.tik.api.tik_scalar.Scalar'> >>> print(type(c_scalar)) <class 'tbe.tik.api.tik_scalar.Scalar'>
那c_scalar就会变成Scalar类型(理当如此,直接用构造函数构造了一个Scalar实例了)。
分例1中,TIK不会返回Tensor或者Scalar,返回的是Expr,Expr不会被编译生成CCEC,即实际上并没有对c_scalar赋值。
分例2中,使用了Scalar的赋值语句来实例化Expr对应的计算,将Expr作为操作数赋值给了新的Scalar变量,并会被编译生成CCEC。
当且仅当Expr作为赋值语句的操作数的时候才会实例化Expr对应的计算。
分例3
除了赋值语句,我们还可以用TIK API的Scalar管理语句set_as来完成对Expr操作数的实例化:
a_scalar = tik_instance.Scalar("int32", "a_scalar", init_value=0) b_scalar = tik_instance.Scalar("int32", "b_scalar", init_value=0) c_scalar = tik_instance.Scalar("int32", "c_scalar", init_value=0) # 1. set_as可以直接接受Expr类型 c_scalar.set_as(a_scalar + b_scalar)
同样分例4的改写也能达到一样的效果。
分例4
a_scalar = tik_instance.Scalar("int32", "a_scalar", init_value=0) b_scalar = tik_instance.Scalar("int32", "b_scalar", init_value=0) c_scalar = tik_instance.Scalar("int32", "c_scalar", init_value=0) # 2. set_as也可以接受Scalar类型 c_expr = a_scalar + b_scalar c_scalar.set_as(c_expr)
为了更加直观地理解表达式和赋值语句之间的区别,我们再来看一个例子:
分例5
a_scalar = tik_instance.Scalar("int32", "a_scalar", init_value=0) b_scalar = tik_instance.Scalar("int32", "b_scalar", init_value=0) c_expr = a_scalar + b_scalar d_scalar = tik_instance.Scalar("int32", "d_scalar", init_value=c_expr) a_scalar.set_as(a_scalar + 1) e_scalar = tik_instance.Scalar("int32", "e_scalar", init_value=c_expr)
从TIK前端表达来看,如果不了解c_expr是一个表达式的概念以及它何时实例化,可能错认为d_scalar和e_scalar的值是一样的,实际上,生成的IR是不一样的,还是那句话,只有当Expr作为赋值语句的操作数时,才会实例化Expr对应的计算。所以在实际的赋值语句中,出现c_expr的地方会被实例化成a_scalar + b_scalar。
从另一个方面也可以认为c_expr是一个a_scalar + b_scalar的别名。
总结
TIK在前端处理的时候,不会将表达式Expr生成IR,遇到赋值语句的时候才会生成IR。
a_scalar = tik_instance.Scalar("int32", "a_scalar", init_value=0) # 生成IR b_scalar = tik_instance.Scalar("int32", "b_scalar", init_value=0) # 生成IR c_expr = a_scalar + b_scalar # 不生成IR d_scalar = tik_instance.Scalar("int32", "d_scalar", init_value=c_expr) # 生成IR a_scalar.set_as(a_scalar + 1) # 生成IR e_scalar = tik_instance.Scalar("int32", "e_scalar", init_value=c_expr) # 生成IR
课后练习
请说明以下两个TIK前端表达在使用上的区别:
- scalar表达
model_point_repeat = tik_instance.Scalar("int16", init_value=model_point_num // VLENFP32) model_point_residual = tik_instance.Scalar("int16", init_value=model_point_num % VLENFP32)
- expr表达
model_point_repeat = init_value=model_point_num // VLENFP32 model_point_residual = init_value=model_point_num % VLENFP32
【参考答案】:
- 第一种是赋值表达,在生成的CCE中会有一个reg_buf去存储model_point_num // VLENFP32的计算量,这样在TIK前端表达中,后面用到model_point_repeat的地方实际上都是用的这个reg_buf寄存器,相当于每次调用存储在这个scalar寄存器中的数值。
- 第二种是表达式的方式,其实在运行到该语句的时候,并没有进行repeat和residual的计算,而是在后续用到这个expr时(赋值语句、计算语句等)才去实例化计算这个表达式的具体数值,这样的具体数值取决于表达式中各个变量当时的数值,如果开发者在编码过程中改变了表达式中某个变量的值,那么后续的值都会发生变化,可能会有意想不到的bug出现。
- 前一种表达会多使用Scalar寄存器,但是能够保证每次用到的数值不会改变(除非重新给它赋值),所以在实际的编码场景中,如果变量只需要用到一次,如repeat一般作为for_range语句的extent,而在别的地方不会使用,则可以直接用expr方式进行表达,如果变量需要多次用到,而且在过程中可能涉及到修改,则用scalar方式表达,可以保证数值的正确性并有效地减少重复的计算。