Users employ API in Python to describe their own network, however, the network construction actually happens in C++. so Protobuf is introduced to send the message between Python and C++.
The Interaction between Python and C++ can be simplified as two steps:
1. C++ tells Python how many Ops there are, and what parameter do users need to offer to initialize a new Op. Python then builds API for each Op at compile time.
2. Users invoke APIs built by Python and provide necessary parameters. These parameters will be sent to C++ fo finish Op construction task.
### Message form C++ to Python
We define a Protobuf message class `OpProto` to hold message needed in the first step. What should an `OpProto` contain? This question is equivalent to “What message do we need to offer, to build a Python API which is legal and user oriented and can use to describe a whole Op.”
Following message are necessary:
1. Op's name, and its simple comment.
2. Input and output variable number; each variable's name, type, and comment.
3. Op's attributes; each attribute includes name, type, comment, **default value** and **value range**.
So `OpProto` can be defined as follows:
```proto
enumAttrType{
INT=1;
FLOAT=2;
STRING=3;
INTS=4;
FLOATS=5;
STRINGS=6;
};
messageAttrValue{
AttrTypetype=1;
optionalintiv=2;
optionalfloatfv=3;
optionalstringsv=4;
repeatedintivs=5;
repeatedfloatfvs=6;
repeatedstringsvs=7;
};
messageAttrProto{
requiredstringname=1;
requiredstringcomment=2;
optionalAttrValuedefault=3;
optionalAttrValuemax=4;
optionalAttrValuemin=5;
requiredAttrTypetype=6;
};
messageVarProto{
requiredstringname=1;
requiredstringcomment=2;
};
messageOpProto{
repeatedVarProtoinputs=1;
repeatedVarProtooutputs=2;
repeatedAttrProtoattrs=3;
requiredstringtype=4;
requiredstringcomment=5;
};
```
The default value and value range didn't appear in out previous design. By adding these two fields, we are able to check attribute validity in Python and find out possible error as soon as possible. What's more, by providing the message about default value and value range to Python docstring, it helps to automatically generate more comprehensive documents.
### Message from Python to C++
To hold message needed in the above second step, we define Protobuf message class `OpDesc`. It is used to hold user-specified parameters in Op describing.
```proto
messageOpDesc{
requiredstringtype=1;
repeatedstringinputs=2;
repeatedstringoutputs=3;
map<string,AttrValue>attrs=4;
};
```
## OpProto Register
Every Op has its own `OpProto`. For using convenience, we need to register them and record all their messages. For each `Op` class, we define a corresponding `OpMaker` class, in whose constructor we implement the `OpProto`'s building process. `OpMaker`'s constructor will be invoked by another function `OpRegistry::RegisterOp()`.
AddAttr("scale","scale of cosine op",float).Default(1.0).LargerThan(0.0);
AddType("cos");
AddComment("This is cos op");
}
}
REGISTER_OP(CosineOp,CosineOpProtoMaker,cos);
```
In `REGISTER_OP(CosineOp, CosineOpProtoMaker, cos)`, we register not only `CosineOp` but also `CosineOpProto`. As fields of `CosineOpProto`, the default value and value range of `scale` are also registered here.
## Python API
Python APIs are divided into two types, high-level API and low-level API.
### High-Level API
High-level API is called by users directly, so it should keep its style consistent with existing V2 APIs.
`hd` is the output of `fc_layer` and it's a `variable`. It can be further sent into other layers as input.
The definition of `fc_layer()`:
```python
deffc_layer(input,size,with_bias,activation):
attr_map={"size":size}
check_attrs(attr_map)
w=make_variable('w')
ifwith_bias:
b=make_variable('b')
else:
b=None
fc_output=make_variable('fc_output');
fc_op(input,w,b,fc_output,attr_map)
act_output=make_variable('sigmod_output');
ifactivation=="sigmod":
sigmod_op(fc_output,act_output);
elif:
# ...
returnact_output;
```
### Low Leval API
In above sample, `fc_op` and `sigmod_op` are low-level API. They build `OpDesc` and invoke corresponding C++ code.
*TODO*
## Op and Kernal
After completely defined, an Op will be run in a network. However, Op's computing method may differ on different devices. One solution is that write an `Op`'s member function `Op::run()`, which contains computing methods of all possible devices. That may be a bad idea because we have to change all `Op`'s code to add a new device.
Another choice is adding a concept named `kernal`. A `Kernal` describes an op's computing process on a certain device. After stripping `Variable` and `kernal`, `Op` becomes a pure conceptual class, which holds neither data nor detailed computing process.
All `Kernal` need to be registered beforehand, just like `Op`.
Now, `Op` is no longer has `Run()` function. It only contains names of variables and kernels. During network running, `RunOp()` is called to invoke `Op`'s corresponding `Kernal`. `get_kernal()` is supposed to return `kernal` for current device.