Intermediate Language

Basic IL Format

The Intermediate Representation may seems simple at the first glance. But when we're writing a real neural network, the model file in JSON IR format is easily getting tens of thousands of lines, which is still piece of cake for computers, but a nightmare for humans.

Thus, LightNet provides a "concise version" of JSON IR format, which contains exactly the same information as JSON IR, called LightNet Intermediate Language (IL), which can be transformed to JSON IR by il2json (installed by default).

The basic format for an operator's representation in IL is:

OPTYPE(TENSOR_IN_ARG_NAME1=TENSOR_IN_NAME1, 
       TENSOR_IN_ARG_NAME2=TENSOR_IN_NAME2, ... |
       TENSOR_OUT_ARG_NAME1=TENSOR_OUT_NAME1,
       TENSOR_OUT_ARG_NAME1=TENSOR_OUT_NAME1, ... |
       PARAM_ARG_NAME1=PARAM_VALUE1,
       PARAM_ARG_NAME2=PARAM_VALUE2, ...);

For example, the example IR used in Intermediate Representation section that composed of 3 operators (create1, slice1, print1) can also be written in IL:

create(| dst=tensor1 | dtype=TL_FLOAT, dims=[2, 4],
       data=[1, 2, 3, 4, 5, 6, 7, 8], ran=[0, 0], from_file=false);
slice(src=tensor1 | dst=tensor2 | axis=1, start=1, len=3);
print(src=tensor2 | | msg="tensor2:");

Save the above code in example.net, and il2json can generate the same code as in the example IR:

$ il2json example.net -o example.json

Then you can feed example.json directly to lightnet. Or you can just combine il2json and lightnet with a pipe:

$ il2json example.net | lightnet -
tensor2:
[[2.000 3.000 4.000]
 [6.000 7.000 8.000]]
info: run time: 0.000062s

Wrap Operators with Macros

The IL interpreter utlizes the C preprocessor of GCC, thus all C preprocessor syntaxes are legal in an IL.

For example, it is common to have a conv2d and a relu operator in a row in NN models. Instead of writing conv2d and relu repeatedly, we can use #define directive to wrap them in a macro for future uses.

#define conv(in_name, out_name, in_c, out_c,                            \
             _size, _stride, _padding, _dilation)                       \
    create(| dst=out_name##_wts |                                       \
           dtype=TL_FLOAT, dims=[out_c, in_c, _size, _size],            \
           ran=[-10, 10], data=[0], from_file=true);                    \
    create(| dst=out_name##_bias | dtype=TL_FLOAT, dims=[out_c],        \
           ran=[-10, 10], data=[0], from_file=true);                    \
    conv2d(src=in_name, weight=out_name##_wts, bias=out_name##_bias |   \
           dst=out_name | group=1, size=[_size, _size],                 \
           stride=[_stride, _stride],                                   \
           padding=[_padding, _padding, _padding, _padding],            \
           autopad="NOTSET",                                            \
           dilation=[_dilation, _dilation])

#define conv_relu(in_name, out_name, in_c, out_c,       \
                  _size, _stride, _padding, _dilation)  \
    conv(in_name, out_name##_conv, in_c, out_c,         \
         _size, _stride, _padding, _dilation);          \
    relu(src=out_name##_conv | dst=out_name |)

The above code first defined a conv macro, which consists of two create operators and a conv2d operator. The two creates create the tensors for the weight and the bias of the conv2d. The conv2d operator does the convolution algorithm using the input tensor named in_name, to the output tensor named out_name, with a weight tensor named out_name##wts and a bias tensor named out_name##bias. The ## is the string-concatation operator in the C preprocessor which concat the macro parameter out_name with wts or bias, so that when we calls conv(tensor1, tensor2,...), the weight tensor gets tensor2_wts and the bias tensor gets tensor2_bias as their names automatically.

Some readers may notice that some macro parameters are prefixed with a _. That's to prevent the parameter names from conflicting with the argument names of the operators in the macro, such as the padding argument of conv2d.

The code then defined a conv_relu macro, which calls the former conv macro and feeds its output to the input of a relu operator.

Embeded Scripts

Sometimes we want some constant expressions to be calculated before we actually translate the IL, which cannot be done by the C preprocessor. So after the C preprocessor, IL supports embeded scripts to process the IL text further.

Embeded scripts are enclosed in ${eval ... } expressions. Suppose we needs to get the anchor number in an object detection network (such as SqueezeDet), whose anchor number can be calculated as

anchor_num = anchors_per_grid * grid_height * grid_width

Using embeded scripts, this may be written as:

#define ANCHORS_PER_GRID 9
#define CONVOUT_H 12
#define CONVOUT_W 20
#define ANCHOR_NUM ${eval ANCHOR_PER_GRID * CONVOUT_H * CONVOUT_W}

Embeded scripts are actually Perl code snippets. So in principle, any legal Perl code can be run in an eval expression.

Warning

Since embeded scripts are Perl code, be cautious that malicious code may do nasty things to your system if an untrusty IL is interpreted with privileged priorities. Of course, there is never a good reason for an IL text to be translated with privileges.