---
# System prepended metadata

title: ClipQ

---

# ClipQ
- A flexible and efficient design and implementation of CNN accelerator with 8-bit CLIP-Q quantization

[TOC]

## Progress 
:::spoiler Milestone
- [x] Setup the env on server (140.116.245.115)
- [x] Full precision training of NIN model on CIFAR-10/100
- [x] Fine-tuning with N-bit Clip-Q
- [x] Inference and check precision 
- [x] Save weight to run on FPGA
:::

![](https://i.imgur.com/aR8wQG3.png)

:::spoiler By week
- Week 15 (2020/12/14-12/18)
    - Identify the problem by understanding PyTorch
        - [PyTorch: Defining new autograd functions](https://pytorch.org/tutorials/beginner/pytorch_with_examples.html#pytorch-defining-new-autograd-functions)
        - [Extending torch.autograd](https://pytorch.org/docs/stable/notes/extending.html#extending-torch-autograd)
        - [Solution found: Difference between apply an call for an autograd function](https://discuss.pytorch.org/t/difference-between-apply-an-call-for-an-autograd-function/13845)
            - API must be changed as follow
            ```python=
            class F_new(torch.autograd.Function):
                @staticmethod
                def forward(ctx, args, gamma):
                    ctx.gamma = gamma
                    pass

                @staticmethod
                def backward(ctx, args):
                    pass

            # Using your old style Function from your code sample:
            F(gamma)(inp)
            # Using the new style Function:
            F_new.apply(inp, gamma)
            ```
            - Fixed in this [commit](https://github.com/WeiCheng14159/caid_clipQ/commit/05e0accf17aaebefc32f54baccd9d739ecf6f4c9) 
    - Fine-tuning with N-bit Clip-Q
        - Best Accuracy: 64.61%
- Week 14 (2020/12/7-12/11)
    - Stuck with the error `RuntimeError: Legacy autograd function with non-static forward method is deprecated. Please use new-style autograd function with static forward method.`
- Week 13 (2020/11/30-12/4)
    - Setup the SW env on server
        - Problem: `pip install -r requirements.txt` fail due to mismatch python environment
            - Solution: Remove version number in `requirements.txt` and install the latest version
        - Stuck on command `Building wheels for collected packages: opencv-python, PyYAML, scandir, visdom, wrapt` 
            - Solution: Restart command
        - `numpy.core.multiarray failed to import` 
            - Solution: Upgrade pip and reinstall numpy
        - `The NVIDIA driver on your system is too old (found version 10010).`
            - Reason: The torch version and nvidia-driver version is different
            - Solution:  Completely uninstall torch & reinstall torch by for cuda 10.1 `# CUDA 10.1
conda install pytorch==1.6.0 torchvision==0.7.0 cudatoolkit=10.1 -c pytorch` 
            - Ref: [Torch official](https://pytorch.org/get-started/previous-versions/)
    - Full precision training of NIN model on CIFAR-100
        - ![](https://i.imgur.com/LWNgPiL.png)
        - Command: `python3 main.py --lr 0.1 --opt SGD --full 1 --cifar 100 --epoch 200`
        - Result: `Best Accuracy: 67.46%` 
        - Command: `python3 main.py --lr 0.1 --opt SGD --full 1 --cifar 10 --epoch 200`
        - Result: `Best Accuracy: 89.30%` 
    - Fine-tuning with N-bit Clip-Q
        - Problem: `RuntimeError: Legacy autograd function with non-static forward method is deprecated. Please use new-style autograd function with static forward method.`
            - Solution: TBD
- Week 12 (2020/11/23-27)
    - Read project doc
    - Read research paper
    - Read core Verilog code (Draw FSM diagram)
    - Identify core python code (NIN training & ClipQ)
:::

## Workflow overview
- Full precision training of NIN model on CIFAR-10/100
- Fine-tuning with N-bit Clip-Q
- Inference and check precision 
- Save weight to run on FPGA
## Software 
### NIN mode
- Structure
    - Three 3x3 conv layer, each 3x3 conv layer is followed by two 1x1 conv layer 
    - ![](https://i.imgur.com/Co5tCbx.png)
- Reason
    - Less parameter, high precision
    - ![](https://i.imgur.com/vVtuAuJ.png)
- Quantized conv layer x9
    - Child class of [torch.nn.quantized](https://pytorch.org/docs/1.7.0/torch.nn.quantized.html?highlight=torch%20nn%20quantized#module-torch.nn.quantized)
- Why 1x1 convolution ? 
    - From [What does 1x1 convolution mean in a neural network?](https://stats.stackexchange.com/questions/194142/what-does-1x1-convolution-mean-in-a-neural-network) 
    - In terms of Google Inception model
        > Suppose this output is fed into a conv layer with $F_1$ 1x1 filters, zero padding and stride 1 ... So 1x1 conv filters can be used to change the dimensionality in the filter space. If $F_1$ > 𝐹 then we are increasing dimensionality, if $F_1$ < 𝐹 we are decreasing dimensionality, in the filter dimension.
    - In terms of channel extension/compression
        > A 1x1 convolution simply maps an input pixel with all it's channels to an output pixel, **not looking at anything around itself**. It is often used to **reduce the number of depth channels**, since it is often very slow to multiply volumes with extremely large depths. 

:::spoiler pytorch code
```python=
class Net(nn.Module):
    def __init__(self,f,cifar,write):
        super(Net, self).__init__()
        self.QCNN = nn.Sequential(
            QConv2d(  3,  96, kernel_size=3, stride=1, padding=1,layer = 1,full=f,w=write),
            QConv2d( 96, 160, kernel_size=1, stride=1, padding=0,layer = 2,full=f),
            QConv2d(160, 192, kernel_size=1, stride=1, padding=0,layer = 3,full=f),
            nn.MaxPool2d(kernel_size=2, stride=2, padding=0),

            QConv2d(192, 96 , kernel_size=3, stride=1, padding=1,layer = 4,full=f),
            QConv2d(96 , 192, kernel_size=1, stride=1, padding=0,layer = 5,full=f),
            QConv2d(192, 192, kernel_size=1, stride=1, padding=0,layer = 6,full=f),
            nn.AvgPool2d(kernel_size=2, stride=2, padding=0),

            QConv2d(192, 384, kernel_size=3, stride=1, padding=1,layer = 7,full=f),
            QConv2d(384, 192, kernel_size=1, stride=1, padding=0,layer = 8,full=f),
            QConv2d(192, int(cifar), kernel_size=1, stride=1, padding=0,layer = 9,full=f),
            nn.AvgPool2d(kernel_size=8, stride=1, padding=0),
            )

    def forward(self, x ):
        for m in self.modules():
            if isinstance(m, nn.BatchNorm2d) or isinstance(m, nn.BatchNorm1d):
                if hasattr(m.weight, 'data'):
                    m.weight.data.clamp_(min=0.01)
        x = self.QCNN(x)
        x = x.view(x.size(0), -1)
        return x
```
:::

### Clip-Q
- Idea:
    - 1) combines **network pruning** and **weight quantization** in a single learning framework that solves for both weight pruning and quantization jointly 
    - 2) makes flexible pruning and quantization decisions that adapt over time as the network structure changes
    - 3) performs pruning and quantization in parallel with **fine-tuning** the **full-precision weights**.
- Algorithm:
    - ![](https://i.imgur.com/5Lna4OC.png)
    - ![](https://i.imgur.com/HJewJAP.png)

- Implementation: `util_write.py`
:::spoiler ClipQ python Code
```python=
def ClipQ(self):
    for index in range(self.num_of_params):
        start = time.time()
        x = self.target_modules[index].data.cpu()
        p=0.4
        b=2
        x1=x.view(-1).numpy()
        x1s=np.sort(x1, axis=None)
        x1arg = np.argsort(x1, axis=None)
        pos = x1s[np.where(x1s>0)]
        pos_arg = x1arg[np.where(x1s>0)]
        neg = x1s[np.where(x1s<0)]
        neg_arg = x1arg[np.where(x1s<0)]

        P_Znum = m.ceil(len(pos)*p)
        N_Znum = m.ceil(len(neg)*p)

        P_max = max(pos[:P_Znum])
        N_min = min(neg[-N_Znum:])

        x1[pos_arg[:P_Znum]]=0
        x1[neg_arg[-N_Znum:]]=0


        x1s=np.sort(x1, axis=None)
        partb0 = m.pow(2,b-1)-1
        partb1 = m.pow(2,b-1)

        pos = x1s[np.where(x1s>0)]
        neg = x1s[np.where(x1s<0)]

        pos_s = (pos[len(pos)-1] - P_max)/partb0
        neg_s = (neg[0] - N_min)/partb1

        pos = pos - P_max
        neg = neg - N_min

        pos_d = {}
        neg_d = {}

        sum_pos = np.zeros(int(partb0))
        sum_neg = np.zeros(int(partb1))

        num_pos = np.zeros(int(partb0))
        num_neg = np.zeros(int(partb1))

        pos_max = np.zeros(int(partb0))
        neg_min = np.zeros(int(partb1))

        for i in range(int(partb0)):
          pos_d[i] = pos[np.where(np.floor(pos/pos_s) == i)] + P_max

        try:
          pos_d[partb0-1] = np.append(pos_d[partb0-1],[pos[len(pos)-1] + P_max])
        except:
          pos_d[partb0-1] = [pos[len(pos)-1] + P_max]

        for i in range(int(partb1)):
          neg_d[i] = neg[np.where(np.floor(neg/neg_s) == i)] + N_min


        try:
          neg_d[partb1-1] = np.append(neg_d[partb1-1],[neg[0] + N_min])
        except:
          neg_d[partb1-1] = [neg[0] + N_min] 

        pos_avg = {}
        neg_avg = {}

        for i in pos_d.items():
          pos_avg[i[0]] = sum(i[1])/(len(i[1])+0.0000001)
          try:
            pos_max[i[0]] = max(i[1])
          except:
            pass

        for i in neg_d.items():
          neg_avg[i[0]] = sum(i[1])/(len(i[1])+0.0000001)
          try:
            neg_min[i[0]] = min(i[1])
          except:
            pass

        xx1 = x1.copy()
        we = x1.copy()
        realW = []

        for i in range(int(partb0)):
          realW.append(pos_avg[i])
          if i==0:
            x1[np.logical_and(xx1>0, xx1<=pos_max[i])] = pos_avg[i]
            we[np.logical_and(xx1>0, xx1<=pos_max[i])] = 1
          else:
            x1[np.logical_and(xx1>pos_max[i-1], xx1<=pos_max[i])] = pos_avg[i]
            we[np.logical_and(xx1>pos_max[i-1], xx1<=pos_max[i])] = i+1


        for i in range(int(partb1)):
          realW.append(neg_avg[i])
          if i==0:
            x1[np.logical_and(xx1<0, xx1>=neg_min[i])] = neg_avg[i]
            we[np.logical_and(xx1<0, xx1>=neg_min[i])] = -1
          else:
            x1[np.logical_and(xx1<neg_min[i-1], xx1>=neg_min[i])] = neg_avg[i]
            we[np.logical_and(xx1<neg_min[i-1], xx1>=neg_min[i])] = -i-1

        x2 = torch.from_numpy(x1) 
        pa = torch.Tensor(realW)
        x=x2.view(x.size())
        num_bits = 8
        num_int = 3
        qmin = -(2.**(num_int - 1))
        qmax = qmin + 2.**num_int - 1./(2.**(num_bits - num_int))
        scale = 1/(2.**(num_bits - num_int))
        xx = x - torch.fmod(x,scale)
        pa = pa - torch.fmod(pa,scale)
        xx[xx.le(qmin)] = qmin
        xx[xx.ge(qmax)] = qmax
        pa[pa.le(qmin)] = qmin
        pa[pa.ge(qmax)] = qmax
        ww = torch.from_numpy(we)
        ww = ww.view(x.size())+2

        if index == 1:  
          if not os.path.exists('./H_data/W2.hex'):
            ch_fileW2(ww,'./H_data/W2.hex')
          if not os.path.exists('./H_data/W8.hex'):
            fileW8(pa,'./H_data/W8.hex',5)

        self.target_modules[index].data = xx.cuda()
```
:::

## Hardware Architecture
### Background
#### Line buffer for conv2d
- Line buffer method first introduced in [Reconfigurable Pipelined 2-D Convolvers for Fast Digital Signal Processing](https://ieeexplore.ieee.org/document/784091)
- ![](https://i.imgur.com/cUNdtUv.png)
- Used in [Going Deeper with Embedded FPGA Platform for Convolutional Neural Network](https://dl.acm.org/doi/10.1145/2847263.2847265)
- ![](https://i.imgur.com/6XByAIM.png)
#### PE method
- PE method [A high performance FPGA-based accelerator for large-scale convolutional neural networks](https://ieeexplore.ieee.org/document/7577308)
- ![](https://i.imgur.com/nFNvEZV.png)
- ![](https://i.imgur.com/zmY0HTe.png)

#### Comparison
|                | Advantage                      | Disadvantage                       |
| -------------- | ------------------------------ | ---------------------------------- |
| Line buffer    | Data reuse for W, In           | Not scalable, Lots of HW resources |
| PE method      | Scalable                       | No data reuse for W                |
### Details ![](https://i.imgur.com/rknRWuu.jpg)
#### Controller `ctrl.v`
- FSM diagram
```graphviz
digraph controller_fsm{
    graph [fontname=Arial];
    node [shape=record,style=filled, fillcolor=aquamarine];
    edge [fontcolor=red];
    // nodes
    s0 [label="S_IDLE"];
    s1 [label="S_READ_PARAM"];
    s2 [label="S_READ_W8"];
    s3 [label="S_READ_W2"];
    s4 [label="S_READ_INPUT"];
    s5 [label="S_EMPTY"];
    s6 [label="S_INPUT_CH"];
    s7 [label="S_FINISH"];

    // edges
    s0->s1 [label="start=1"];
    s1->s2 [label="s1_fin=1"];
    s2->s3 [label="s2_fin=1"];
    s3->s4 [label="s3_fin=1"];
    s4->s5 [label="s4_fin=1"];
    s5->s6 [label="s5_fin=1"];
    s6->s7 [label="s6_fin=1"];
    s6->s3 [label="s6_back=1"];
}
```
#### Conv Unit `c_unit.v`
- Diagram
```graphviz
digraph accu_module{
    rankdir=TB;
    // splines=false;
    graph [fontname=Arial];
    node [shape=record,style=filled, 
        fillcolor=aquamarine,fontsize=20.0];
    edge [fontcolor=red, fontsize=20.0];
    
    subgraph cluster_0{
        label="c_unit_0";
        // node
        mult_ [label="*"];
        w_ [label="weight"];
        add_ [label="+"];
        out_prev_ [label="accu_prev"];
        out_ [label="accu"];
        in_ [label="input"];
        // edge
        mult_->add_[label="mul_r[15:0]"];
        w_->mult_[label="w_in[7:0]"];
        in_->mult_[label="in[7:0]"];    
        out_prev_->add_[label="accu_prev[31:0]"];
        add_->out_[label="accu[31:0]"];
        out_->out_prev[label=""];
    };
    
    subgraph cluster_1{
        label="c_unit_1";
        // node
        mult [label="*"];
        w [label="weight"];
        add [label="+"];
        out_prev [label="accu_prev"];
        out [label="accu"];
        in [label="input"];
        out_next [label="accu_next"];
        // edge
        mult->add[label="mul_r[15:0]"];
        w->mult[label="w_in[7:0]"];
        in->mult[label="in[7:0]"];    
        out_prev->add[label="accu_prev[31:0]"];
        add->out[label="accu[31:0]"];
        out->out_next[label=""];
    };
}
```
:::spoiler Verilog code
```c=
module c_unit(
  clk,rst,
  l_in,
  d_in,
  en_pu,
  en_in,
  en,
  zero_en,
  w_en,
  w_in,
  d_out,
  w_out
);

input  clk,rst;
input signed [7:0]  d_in;
input signed  [31:0] l_in;

input en_pu;
input en_in;
output reg en;
input zero_en;

input  [7:0]  w_in;
input         w_en;

output reg signed [31:0] d_out;
output reg signed [7:0]  w_out;

wire signed [ 7:0]in;
wire signed [31:0]la;

wire signed [15:0]mul_r;

reg signed [31:0]out_reg;


//w_out
always@(posedge clk or negedge rst)begin
  if(!rst)begin
    w_out <= 0;
  end
  else begin
    if(w_en)
      w_out <= w_in;
  end
end

//in
//assign in = en? d_in :  7'b0;
//assign la = en? l_in : 31'b0;

//mul_r & d_out
assign mul_r = w_out * d_in;

//out_reg
always@(posedge clk or negedge rst)begin
  if(!rst)begin
    out_reg <= 0;
  end
  else begin
    out_reg <= mul_r + l_in;
  end
end

//d_out
always@(*)begin
  d_out = out_reg;
  if(!en)
    d_out = 0;
end

//en
always@(posedge clk or negedge rst)begin
  if(!rst)begin
    en <= 0;
  end
  else begin
    if(zero_en)
      en <= 0;
    else if(en_pu)
      en <= en_in;
  end
end

endmodule
```
:::

:::spoiler 5x5 design diagram
![](https://i.imgur.com/VKJlbkW.png)
:::

- Average Weight Selection TBD
