2018年11月18日 星期日

Spatial Tutorial - Hello, Spatial!

在這一節, 將學習在 Spatial 中有關下列元件:
  • 應用程式骨架( import 宣告, 應用程式創建, Accel 範圍, Host 範圍)
  • DRAM
  • SRAM
  • ArgIn
  • ArgOut
  • HostIO
  • Reg
  • 型別系統(Typing system)
  • host 與 accel 間的資料傳輸
  • 基本的 debugging 攔截
請注意大量的 Spatial 應用程式可以在此找到.

Overview

在本節中, 將了解如何組合建立最基本的 Spatial 應用程式. 由於這些程式碼並沒有做任何”有意義” 的工作, 它演示了幾乎所有應用程式都使用的基本指令, 並且旨在作為硬體的 “Hello, world!” 程式. 以產生作為加速器與 host 互動的輸入與輸出暫存器作開始, 接著加入在 off-chip DRAM 與 on-chip SRAM 之間的 tile 傳輸. 然後學習哪些利用 Host 提供作為測試的功能. 最後將學習針對不同輸出基本的編譯流程: 測試演算的功能性, 所產生 RTL 的 cycle-accurate simuation, 以及對支援的 FPGA 與架構產生佈署用的 bitstream.
上圖為在本教學中的操作的視覺化. 一開始有著 Host 與 FPGA, 兩者都連接著 DRAM. 然後將實體化所有可以讓兩個處理器互動的不同方法. 將創建位於 FPGA 之中的 RTL, 以及一些在 Host 中的 C++ 程式碼. Spatial 自動地實體化一個稱為 “Fringe” 的盒框, 這是一個 FPGA-agnostic (champ: 表示著可以跨越不同的 FPGA) 的硬體設計, 允許 RTL 與周邊, DRAM, PCIe 匯流排以及在 SoC 或 FPGA 板上俱備的其他硬體作互動.

Application Template

所有 Spatial 程式有著一些基本元件. 下列的程式碼在一個稱為 HelloSpatial 的應用中示範了每一個那些元件. 這是一個完整的應用, 能夠任意嘗試與編譯它. 它並沒有作任何事情, 但也不會阻止你如何地使用它.
針對特定平台編譯此應用程式, 請參考 Target 頁面
import spatial.dsl._
@spatial object HelloSpatial extends SpatialApp {
  def main(args: Array[String]): Void = {
    // Host Code
    Accel {
      // Acceleratable Code
    }
    // More Host Code
  }
}

ArgIn/ArgOut Interfaces

接著繼續發展這個應用程式, 加入 ArgIns, ArgOuts 與 HostIOs.
最基本讓資料進出 FPGA 的方式是在 Accel 與 Host 之牽傳遞個別參數. 一些 ArgIns 的使用範例像是: 傳遞應用參數到 Accel, 像是在 PageRank 中的 damping factor 或是像是 GEMM 等應用的資料結構的維度; 以及 ArgOuts 的使用範例像是: 輸出內積的純量結果. 將在 Accel 區塊之上定義一些暫存器, 以便 CPU 能夠配置它們.
這時, 可能已經注意到持續以中括號指定所有為 Int. 中括號是 Scala 如何傳遞型別參數的方式. Spatial 是一個硬體語言, 支援了一些標準 32-bit 整數外的型別, 能夠任意的定義使用他們. 一些範例包含了:
type T = FixPt[FALSE,_16,_0]
type Flt = Float
請注意在 println, 雙引號前帶有 “r” 字母. 這會在程式運作時透過 ${value} 以其實際數值的形式替換字串.
import spatial.dsl._
@spatial object HelloSpatial extends SpatialApp {
  def main(args: Array[String]): Void = {
    // Create Args
    val x = ArgIn[Int]
    val y = ArgOut[Int]
    val z = HostIO[Int]
    
    // Set `x` to the value of the first command line argument
    setArg(x, args(0).to[Int])
    // Set `z` to the value of the second command line argument
    setArg(z, args(1).to[Int])
    
    Accel {
      // Set `y` to the sum of `x` and `z`
      y := x + z
    }
    // Report the answer
    println(r"Result is ${getArg(y)}")
  }
}

Reg, SRAM, and Basic Control

接著將展示如何加入記憶體結構到 Accel. 對於一個完整的記憶體列表, 請參考說明文件. 在這個範例中僅示範 Reg 與 SRAM.
在下列程式碼中, 取得參數 x. 並且使用此來索引 Accel 中的一個記憶體. 範例中建立了記憶體 s 與 r. 在 Spatial 中 SRAM 最高能以 5D 的方式創建, 而 Reg 為 single-element 資料結構.
這裡建立了 Foreach 迴圈來存取 s 的每個單位. 第一個針對 Foreach 的參數集合指定了迴圈的 counter 範圍. 在此範例中, 指出第一個迴圈索引最高到 16, 而第二個應最高到 32. 最常見的是,迴圈範圍可以指定為:
 until  by  par 
在此例中, 16 by 1 等同於 0 until 16 by 1 par 1. 下限, 上限與步距都允許負值,然而 par 必須為正值. 後續的教學將討論更多複雜的控制結構與 counter 的使用情況. 但現在不會平行或巢狀任何迴圈.
第二個針對Foreach 迴圈的參數綁定了 counter 數值到此例中的變數ij中.這能指定每個 iteration 所採取的行為.
請注意為了寫入到SRAM , 必須指定位置並且使用=. 為了寫入到Reg, 必須使用:=.要自SRAM 讀取能夠直接傳入位置, 而要自Reg讀取必須使用.value.
import spatial.dsl._

@spatial object HelloSpatial extends SpatialApp {
  def main(args: Array[String]): Void = {
    // Create ArgIn
    val x = ArgIn[Int]
    
    // Set `x` to the value of the first command line argument
    setArg(x, args(0).to[Int])
    
    Accel {
      // Create 16x32 SRAM and a Register
      val s = SRAM[Int](16,32)
      val r = Reg[Int]
      
      // Loop over each element in SRAM
      Foreach(16 by 1, 32 by 1){(i,j) => 
        s(i,j) = i + j
      }
      // Store element into the register, based on the input arg
      r := s(x,x)

      // Print value of register (only shows in Scala simulation)
      println(r"Value of SRAM at (${x.value},${x.value}) is ${r.value}")
    }

  }
}

DRAMs and Transfers

在多數的情形, 會想使用 Spatial 來撰寫利用 FPGA 作為加速器來分擔 CPU 計算的應用程式. 這表示將有大量的資料結構想要在此兩著間共享. 為此目的導入了DRAM, 藉此資料結構能夠被 CPU 所配置並且能夠被 FPGA 所讀寫.
在下面的例子, 創建了 DRAM , 在 Host 端設定了 DRAM 的內容,告知加速器修改內容與自 Host 讀回. 建立一個 DRAM 實際上是一個 malloc, Host 配置一個記憶體區域並且傳遞此區域的 pointer 到 FPGA. 接著在 Host 端使用Array建立一個資料結構, 並且對此陣列每個單位填入數值 0. 請參見文件來以更複雜的方式建立 Arrays, Matrices, and Tensors. 以Array 的內容來設定 DRAM,本質上為memcpy此記憶體區域.
藉由DRAM的設定與載入, 能夠建立一個在加速器中的 local memory s, 並且傳送d的內容到此SRAM. 請注意資料並不能在 DRAM 中直接操作, 並且自 DRAM 到 SRAM 的傳輸是以 burst-granularity 的方式進行. 這表示對一個像是 ZC706 SoC, 一個自 DRAM 的 burst 提供了 512 bits. 若載入了一個無法有效以 burst 大小整除的單位數目, 將犧牲一些頻寬在被加速器所忽略的資料上.
在此例中, 使用(0::16)來載入並存取自d. 這表示自位置 0 到位置 16 的單位將依序傳輸. 更常見的是, 可能以語法(:: by par )加入了平行化. 有著平化參數允許傳輸硬體在 bus 上同時放置更多單位, 並且以額外的硬體開銷提升了傳輸的速度.
使用一個 1D 的Foreach來修改在s中的每個單元,接著寫回到原本的DRAM. 在加速器之外, 能夠使用printArray來讓 Host 印出記憶體內容.
如同 SRAM, DRAM 能最高有著 5D.
import spatial.dsl._

@spatial object HelloSpatial extends SpatialApp {
  def main(args: Array[String]): Void = {
    // Create DRAM (malloc)
    val d = DRAM[Int](16)
    
    // Set DRAM (memcpy)
    val data = Array.fill[Int](16)(0)
    setMem(d, data)
    
    Accel {
      // Create 16-element SRAM
      val s = SRAM[Int](16)
      
      // Transfer data from d to s
      s load d(0::16)
    
      // Add number to each element
      Foreach(16 by 1){i => s(i) = s(i) + i}

      // Transfer data back to d
      d(0::16) store s
    }
    
    // Print contents in memory
    printArray(getMem(d), "Result: ")

  }
}

沒有留言:

在 ARM 平台上使用 Function Multi-Versioning (FMV) - 以使用 Android NDK 為例

Function Multi-Versioning (FMV) 過往的 CPU 發展歷程中, x86 平台由於因應各種應用需求的提出, 而陸陸續續加入了不同的指令集, 此外也可能因為針對市場做等級區隔, 支援的數量與種類也不等. 在 Linux 平台上這些 CPU 資訊可以透過...