Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Deep Learning in Go, or Shennanigans With Matrices

3e45c02f2ae5f812a55c4975124da6b2?s=47 Xuanyi
October 29, 2015

Deep Learning in Go, or Shennanigans With Matrices

Presented at Golang Sydney October 2015. I rushed through a lot of the slides. Write up to come

3e45c02f2ae5f812a55c4975124da6b2?s=128

Xuanyi

October 29, 2015
Tweet

Transcript

  1. @chewxy  on  Twi-er   Deep  Learning  in  Go   Or,

     shennanigans     By  Xuanyi  Chew,  GolangSyd  Oct2015  
  2. @chewxy  on  Twi-er   Why  Go?   •  Knee  jerk

     reacDon.   •  No  good  reason.     •  You  probably  should  use  Theano.  
  3. @chewxy  on  Twi-er   Deep  Neural  Network   Picture thanks

    to Michael Nielsen
  4. @chewxy  on  Twi-er   Modern  Deep  Nets  (LSTM)  

  5. @chewxy  on  Twi-er   A  Neurone   Picture thanks to

    Michael Nielsen f(x, w) w1 w2 w3 output = f(Σwi xi )
  6. @chewxy  on  Twi-er   Many  Neurones   Picture thanks to

    Michael Nielsen
  7. @chewxy  on  Twi-er   Representing  Neural  Networks   w1  

    w2   w3   Input Output (number of neurones)
  8. @chewxy  on  Twi-er   Deep  Neural  Network   Picture thanks

    to Michael Nielsen 1 x n n x m m x o o x 4
  9. @chewxy  on  Twi-er   Implementing  a  NN  in  Go  

      type  NeuralNetwork  struct  {    DeepConfig      hiddenLayers  []*NeuroneLayer    logitLayer  *LogitLayer  //  or  SoftmaxLayer   }    
  10. @chewxy  on  Twi-er   Implementing  a  NN  in  Go  

    type  NeuroneLayer  struct  {    *Neurone    LayerConfig      activationFunction  ActivationFunction   }         type  ActivationFunction  func(float64)  float64  
  11. @chewxy  on  Twi-er   Implementing  a  NN  in  Go  

    type  Neurone  struct  {    //  weights[i][j]  -­‐-­‐  i  is  the  output  index,  j  is  the  input  index    Weights  [][]float64      Biases    []float64   }  
  12. @chewxy  on  Twi-er   Implementing  a  NN  in  Go  

    func  (nl  *NeuroneLayer)  Activate(x  []float64)  []float64  {    retVal  :=  make([]float64,  nl.Outputs)    for  i,  row  :=  range  nl.Weights  {      var  output  float64      for  j,  weight  :=  range  row  {        output  +=  weight  *  x[j]      }      output  +=  nl.Biases[i]      retVal[i]  =  nl.activationFunction(output)    }    return  retVal   }  
  13. @chewxy  on  Twi-er   Implementing  a  NN  in  Go  

    •  Other  bits  and  bobs:   •  Back  propagate  errors   •  Learning  method  (SGD,  AdaGrad,  RMSprop,  L-­‐BFGS…)   •  Errors  (Cross  Entropy,  meansquare  errors…)   •  Unique  layers  (ConvoluDon,  RBM,  etc  …)   •  Mix  and  Match  bits   •  All  the  above  is  just  simple  manipulaDon  of  matrix  code  
  14. @chewxy  on  Twi-er   The  Problem   •  Not  fast

     enough   •  Use  too  much  memory  
  15. @chewxy  on  Twi-er   Too  Much  Memory   •  Some

     matrices  are  sparse   •  Too  much  space  wasted  storing  0.0f  
  16. @chewxy  on  Twi-er   Real  Life  Logs   2015/10/21  09:59:14

     Epoch  58.  Correct/Total:  214577/229672  =  0.93   2015/10/21  09:59:14  MatSize:  6370341/151970075   2015/10/21  09:59:14  Mat  load  factor:  0.04   2015/10/21  09:59:14  Allocated                    :  1905.51  MB   2015/10/21  09:59:14  Total  Allocated        :  244659.96  MB   2015/10/21  09:59:14  Heap  Allocted            :  1905.51  MB   2015/10/21  09:59:14  Sys  Total  Allocated:  2335.47  MB   2015/10/21  09:59:14  -­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐        
  17. @chewxy  on  Twi-er   Solution(s)   •  Implement  Sparse  Matrix

      •  map[coordinate]float64   •  type  sparseMat64  struct  {    d  []float64    r  []uint32    c  []uint32   }   •  type  sparseMat64  []*cell   •  type  sparseMat64  [][MAXCOL]float64   type  coordinate  struct  {  r,  c  int  }   type  cell  struct  {  col  int,  val  float64  }  
  18. @chewxy  on  Twi-er   Solution(s)   •  Implement  Sparse  Matrix

      •  map[coordinate]float64   •  type  sparseMat64  struct  {    d  []float64    r  []uint32    c  []uint32   }   •  type  sparseMat64  []*cell   •  type  sparseMat64  [][MAXCOL]float64     type  coordinate  struct  {  r,  c  int  }   type  cell  struct  {  col  int,  val  float64  }     Most flexible Most useful for Hadamard Fastest, good for small MAXCOL Fastest to build
  19. @chewxy  on  Twi-er   Dense  Matrix   •  "import  github.com/gonum/matrix"

      •  [][]float64   •  [][MAXCOL]float64   •  [MAXCOL][]float64   •  type  mat64  struct  {    d      []float64    stride  int   }  
  20. @chewxy  on  Twi-er   Other  Memory  Stuff   •  Transpose

     at  will  or  store  both  Column-­‐Major  and  Row-­‐Major   matrix?   •  Layer  by  layer  training  (save  each  layer  to  disk)  
  21. @chewxy  on  Twi-er   Not  Fast  Enough   •  Let's

     look  at  a  real  life  example  
  22. @chewxy  on  Twi-er   Embarrassingly  Parallel   threads  :=  runtime.GOMAXPROCS(-­‐1)

      chunkSize  :=  len(mat)  /  threads     costChan  :=  make(chan  *cost)   for  chunk  :=  0;  chunk  <  threads;  chunk++  {    var  start  int  =  chunk  *  chunkSize    var  end  int      if  start+chunkSize  >  len(mat)  {      end  =  len(mat)    }  else  {      end  =  start  +  chunkSize    }      go  nn.costFn(mat[start:end],  chunkSize,  costChan)   }  
  23. @chewxy  on  Twi-er   Problem   •  I  only  have

     8  CPUs  
  24. @chewxy  on  Twi-er   ProJiling…   Showing  top  10  nodes

     out  of  61  (cum  >=  44.95s)              flat    flat%      sum%                cum      cum%        254.90s  67.81%  67.81%        337.61s  89.81%    github.com/chewxy/goddamnproject/dp. (*neuralnetwork).costFn          11.39s    3.03%  70.84%          11.39s    3.03%    sync/atomic.CompareAndSwapUint32                  8s    2.13%  72.97%          29.19s    7.77%    sync.(*Mutex).Lock            7.51s    2.00%  74.96%            7.51s    2.00%    runtime.procyield            7.48s    1.99%  76.95%            7.69s    2.05%    github.com/chewxy/goddamnproject/dp. (*neuralnetwork).computeScores            6.92s    1.84%  78.80%            8.04s    2.14%    runtime.mapaccess2_fast64            6.25s    1.66%  80.46%          13.54s    3.60%    math.Pow            5.17s    1.38%  81.83%            5.17s    1.38%    sync/atomic.AddUint32            4.08s    1.09%  82.92%            4.08s    1.09%    runtime.heapBitsForObject            3.40s      0.9%  83.82%          44.95s  11.96%    math/rand.(*lockedSource).Int63   (pprof)  weblist  costFn  
  25. @chewxy  on  Twi-er   weblist  cosFn   225  .  

                 .            //  This  loop  took  a  helluva  long  time     226  4.93s            4.93s        for  _,  a  :=  range  actives  {     227  29.47s          29.47s  layer2Grad[i][a]  +=  delta  *  hiddenAct[a]     228  1.02mins      1.02mins        hiddenActGrad[a]  +=  delta  *  nn.weights2[i][a]   229            .      .            }    
  26. @chewxy  on  Twi-er   AVX  to  the  Rescue!   • 

    Intel®  brings  extra  extra  parallelism  to  SIMD  instrucDons  with   AVX©®™  
  27. @chewxy  on  Twi-er   AVX/SIMD  in  a  nutshell   256

     bit  register   float64   float64   256  bit  register   float64   float64   +
  28. @chewxy  on  Twi-er   AVX/SIMD  in  a  nutshell   256

     bit  register   float64   float64   256  bit  register   float64   float64   + Single Instruction Multiple Data VADDPD
  29. @chewxy  on  Twi-er   AVX  in  Go   /*  

    #cgo  CFLAGS:  -­‐mavx  -­‐std=c99   #include  <stdio.h>   #include  <stdlib.h>   #include  <immintrin.h>//AVX:  -­‐mavx     void  avx_add64(const  size_t  n,  double  *x,  double  *y,  double  *z)  {    static  const  size_t  elements  =  4;  //  with  double  precision,   the  processor  can  only  handle  4  at  a  time.  Maybe  switch  to  float32?    const  size_t  end  =  n  /  elements;      //  convert  to  AVX  specific  types    __m256d  *vz  =  (__m256d  *)z;    __m256d  *vx  =  (__m256d  *)x;    __m256d  *vy  =  (__m256d  *)y;      for  (size_t  i=0;  i  <  end;  ++i  ){      vz[i]  =  _mm256_add_pd(vx[i],  vy[i]);    }   }    
  30. @chewxy  on  Twi-er   AVX  in  Go   //  multiplies

     a  scalar  by  a  vector   void  avx_scale64(const  double  *s,  const  size_t  n,  double  *x,    double   *z)  {      static  const  size_t  elements  =  4;  //  with  double  precision,   the  processor  can  only  handle  4  at  a  time.  Maybe  switch  to  float32?    const  size_t  end  =  n  /  elements;      const  __m256d  vy    =  _mm256_broadcast_sd(s);  //  broadcast  a   scalar  into  vy      //  load  x  and  z  into  AVX  dedicated  registers    __m256d  *vz  =  (__m256d  *)z;    __m256d  *vx  =  (__m256d  *)x;      for  (size_t  i=0;  i  <  end;  ++i)  {      vz[i]  =  _mm256_mul_pd(vx[i],  vy);    }   }     */  
  31. @chewxy  on  Twi-er   AVX  in  Go   func  malloc64(length

     int)  []float64  {    //  pad  it  so  that  at  most  there  will  be  64  items   extra  -­‐  worse  comes  to  worst,  there  are    a  bunch  of  0s  to   add  or  mul    size  :=  length  *  64      ptr  :=  C._mm_malloc((C.size_t)(size),  64)    header  :=  reflect.SliceHeader{      Data:  uintptr(unsafe.Pointer(ptr)),      Len:    length,      Cap:    length,    }    goSlice  :=  *(*[]float64)(unsafe.Pointer(&header))    return  goSlice   }     //  somewhere  later   defer  C._mm_free(unsafe.Pointer(&vec[0]))  
  32. @chewxy  on  Twi-er   The  Bits  To  Be  AVX'd  

    for  i  :=  range  rows  {    //  calculate  delta  omitted    //  This  loop  took  a  helluva  long  time    for  _,  a  :=  range  actives  {      layer2Grad[i][a]  +=  delta  *  hiddenAct[a]      hiddenActGrad[a]  +=  delta  *  nn.weights2[i][a]    }   }  
  33. @chewxy  on  Twi-er   After  AVX   for  i  :=

     range  rows  {    //  calculate  delta  omitted    scale64(delta,  hiddenAct)        ith2LWeights  :=  make([]float64,  len(nn.weights2[i]))    copy(nn.weights2[i][0:],  ith2LWeights[0:])    scale64(delta,  ith2LWeights)    dropout(ith2LWeights,  inactives)      add64(layer2Grad[i],  hiddenAct)    add64(hiddenActGrad,  ith2LWeights)   }  
  34. @chewxy  on  Twi-er   Uh  Oh   Showing  top  10

     nodes  out  of  92  (cum  >=  13.03mins)              flat    flat%      sum%                cum      cum%      4.93mins  11.45%  11.45%      4.93mins  11.45%    runtime._ExternalCode      4.58mins  10.62%  22.07%    10.03mins  23.28%    runtime.cgocall      3.26mins    7.56%  29.62%      3.26mins    7.56%    runtime.heapBitsForObject      3.24mins    7.53%  37.15%    26.96mins  62.59%    github.com/chewxy/ goddamnproject/dp.(*neuralnetwork).costFn      2.75mins    6.38%  43.53%      3.45mins    8.01%    runtime.greyobject      2.20mins    5.11%  48.64%      2.20mins    5.11%    runtime.memmove      1.74mins    4.03%  52.67%      8.25mins  19.15%    runtime.scanobject      1.71mins    3.96%  56.63%      1.71mins    3.96%    runtime.memclr      1.45mins    3.38%  60.00%      3.13mins    7.26%    runtime.deferreturn      1.34mins    3.12%  63.12%    13.03mins  30.24%    runtime.systemstack   (pprof)  weblist  costFn    
  35. @chewxy  on  Twi-er   Overhead  for  CGo   •  Is

     way  too  high     217  1.99s      4.92mins      scale64(delta,  hiddenAct)      ...   221  3.74s      5.05mins      scale64(delta,  ith2LWeights)      ...   228  3.84s      6.48mins      add64(layer2Grad[i],  hiddenAct)     229  1.52s      6.07mins      add64(hiddenActGrad,  ith2LWeights)    
  36. @chewxy  on  Twi-er   Solution(s)   •  Batch  Cgo  calls

      •  Handwrite  ASM,  match  APIs  
  37. @chewxy  on  Twi-er   Solution:    Don't  Use  Cgo  

    •  Batch  Cgo  calls   •  Handwrite  ASM!   Too much rearchitecting
  38. @chewxy  on  Twi-er   Problem   •  Go's  Assembler  doesn't

     support  AVX  instrucDons.   •  Stuck  with  SSE2   •  golang.org/src/cmd/internal/obj/x86/a.out.go  lists  all  the   instrucDons  and  registers  supported  
  39. @chewxy  on  Twi-er   //  func  scale64(s  float64,  a  []float64)

      TEXT  ·∙scale64(SB),  7,  $0      MOVSD  s+0(FP),  X0    MOVQ  x_data+8(FP),  SI    //  move  the  first  element  to  the  indexing  register    MOVQ  x_len+24(FP),  BP    //  move  the  length  into  BP      //  check  that  there  is  more  than  4  values  in  the  slice    SUBQ    $4,  BP    JL  remainders      //  jump  to  remainder    //  manually  broadcast  s  into  X0's  lower  bits,  which  is  used  for  SIMD    MOVLHPS  X0,  X0     loop:    //  load  the  first  two  elements,  and  then  multiply  by  s  (which  is  in  X0)    MOVUPD  (SI),  X2          MULPD  X0,  X2    //  load  second  two  elements,  then  multiply  by  s    MOVUPD  16(SI),  X4        MULPD  X0,  X4    //  move  the  values  back  to  the  original  positions    MOVUPD  X2,  (SI)    MOVUPD  X4,  16(SI)      //  now  that  we  are  done  with  the  first  4  elements,  update  the  pointers  to  the  top  of  the   next  4  elements    ADDQ  $32,  SI      SUBQ  $4,  BP      //  the  len  is  now  less  4    JGE    loop     remainders:    ADDQ  $4,  BP      //  we  subtracted  earlier,  remember??!!!    JEQ    done    //  if  there  are  nothing  left  to  process,  we'll  go  to  done     remainingloop:    //  load  element  from  x,  and  then  multiply  by  s  (which  in  X0)      //  TODO:  check  if  there  is  a  performance  penalty  on  using  SSE  registers    MOVSD  (SI),  X2    MULSD  X0,  X2      //  save  it  back  to  the  array    MOVSD  X2,  (SI)      //  update  the  pointer    ADDQ    $8,  SI      DECQ  BP    JNE  remainingloop   done:    RET  
  40. @chewxy  on  Twi-er   Pprof  Says…   217    1.64s

             12.82s    scale64(delta,  hiddenAct)      ...   221    7.88s            7.88s    scale64(delta,  ith2LWeights)      ...   228        .                    .      add64(layer2Grad[i],  hiddenAct)     229        .                    .    add64(hiddenActGrad,  ith2LWeights)    
  41. @chewxy  on  Twi-er   Interesting  Note   go  test  -­‐bench

     .   testing:  warning:  no  tests  to  run   PASS   BenchmarkGoScale64-­‐8    1000000              1339  ns/op   BenchmarkScale64-­‐8        5000000                329  ns/op   BenchmarkGoAdd64-­‐8        1000000              1711  ns/op   BenchmarkAdd64-­‐8            1000000              1473  ns/op   ok      github.com/chewxy/testideas3  6.567s     func  GoAdd64(a,  b  []float64)  {    for  i,  v  :=  range  a{      a[i]  =  v  +  b[i]    }   }   Not much difference
  42. @chewxy  on  Twi-er   Other  More  Realistic  Solutions   • 

    Be-er  algorithms   •  PrecalculaDng  acDvaDons  and  caching§   •  Binary  weights  (no  mulDplicaDon,  only  addiDon)*     *very  very  very  very  very  new.  I'm  sDll  playing  with  it    
  43. @chewxy  on  Twi-er   GoNum   •  Failed  build  (a

     few  months  ago  when  I  started)…  haven't   checked  since.  
  44. @chewxy  on  Twi-er   Conclusion   •  Building  a  neural

     network  in  Go  is  fairly  simple.   •  Building  a  performant  neural  network  in  Go?  Only  embark   with  quesDonable  sanity   •  Irony:  deploying  Theano  is  hard.  But  all  my  opDmizaDons   make  deploying  this  equally  hard.   •  A  default  built  in,  well  opDmized  matrix  type  would  be  really   really  really  sweet