2 ポイント 投稿者 GN⁺ 2024-04-27 | 1件のコメント | WhatsAppで共有
  • tiny-gpuは、GPUがハードウェアレベルでどのように動作するかを基礎から学ぶためのVerilogベースの最小GPU実装であり、グラフィックス専用ハードウェアよりもGPGPUやMLアクセラレータの共通原理に重点を置いている
  • 実装は、15個未満の文書化されたVerilogファイル、アーキテクチャとISAドキュメント、行列加算・乗算カーネル、カーネルのシミュレーションと実行トレース対応で構成される
  • GPUは一度に単一カーネルを実行し、プログラムメモリとデータメモリをロードしてthread_countを設定した後、start信号を立ててカーネルを開始する
  • 単純化のため、各コアは一度に1つのblockを処理し、各スレッドはALU・LSU・PC・レジスタファイルを持つが、すべてのスレッドが各命令の後に同じPCへ収束すると仮定している
  • 現代GPUの多層キャッシュ、共有メモリ、メモリコアレッシング、パイプライニング、ワープスケジューリング、分岐ダイバージェンス、バリアといった機能の大半は除外し、学習向けの構造を優先している

tiny-gpuが解決しようとする問題

  • CPUにはアーキテクチャから制御信号まで学べる資料が多い一方で、現代GPUの低レベルな技術的詳細は競争の激しい市場のため大半が独占的なままになっている
  • GPUプログラミングに関する資料は多いが、GPUがハードウェアレベルでどのように動作するかを学べる資料はほとんどない
  • オープンソースGPU実装であるMiaowVeriGPUは、機能の完成度と動作を目標としているため構造が複雑である
  • tiny-gpuは製品レベルのグラフィックスカードにある多くの複雑さを取り除き、現代のハードウェアアクセラレータに共通する中核要素へ集中している
    • GPUアーキテクチャの重要な構成要素
    • SIMDプログラミングモデルがハードウェアで実装される方式
    • 限られたメモリ帯域幅をGPUが扱う方式

全体アーキテクチャ

  • tiny-gpuは一度に1つのカーネルだけを実行するよう設計されている
  • カーネル実行手順は次のとおり
    • グローバルプログラムメモリにカーネルコードをロード
    • データメモリに必要なデータをロード
    • デバイス制御レジスタに実行するスレッド数を指定
    • start信号をhighに設定してカーネルを実行
  • GPUは次のユニットで構成される
    • デバイス制御レジスタ
    • ディスパッチャ
    • 可変個数のコンピュートコア
    • データメモリとプログラムメモリ用のメモリコントローラ
    • キャッシュ

カーネル実行とスレッド割り当て

  • デバイス制御レジスタはカーネル実行メタデータを保存する役割を持ち、tiny-gpuでは実行する総スレッド数であるthread_countだけを保存する
  • ディスパッチャはカーネル開始時にスレッドを複数のコンピュートコアへ分配する
    • 並列実行可能なスレッドの束をblockとして構成する
    • 利用可能なコアへblockを送って処理する
    • すべてのblockの処理が終わるとカーネル実行完了を通知する
  • 単純化されたコアは一度に1つのblockを処理する
  • 各スレッドは専用のALU、LSU、PC、レジスタファイルを持つ
  • これらの資源上でスレッド命令実行を管理することが、GPUの難しい課題の1つである

メモリ構造とコントローラ

  • GPUは外部グローバルメモリとインターフェースするよう作られており、単純化のためデータメモリとプログラムメモリを分離している
  • データメモリ仕様
    • 8ビットアドレッシング
    • 合計256行
    • 8ビットデータ
    • 各行は256未満の値を保存
  • プログラムメモリ仕様
    • 8ビットアドレッシング
    • 合計256行
    • 16ビットデータ
    • ISAに従って各命令は16ビット
  • メモリコントローラはコアから来るメモリ要求を追跡し、実際の外部メモリ帯域幅に合わせて要求を制限し、応答を正しい資源へ渡す
  • 各メモリコントローラはグローバルメモリ帯域幅に応じた固定数のチャネルを持つ
  • キャッシュは作業中の機能であり、外部メモリから取得したデータをデバイス内SRAMに保存することで、その後の要求でより高速に取得でき、新しいデータにメモリ帯域幅を使えるようにする

コア内部構成

  • 各コアにはスレッド実行を管理する単一のスケジューラがある
  • tiny-gpuのスケジューラは1つのblockの命令を最後まで実行してから新しいblockを取得し、すべてのスレッド命令を同期された順序で実行する
  • より高度なスケジューラでは、パイプライニングやワープスケジューリングによって資源利用率を高められる
  • スケジューラの主な制約は、グローバルメモリからデータをロード・保存するときに発生するレイテンシである
    • ほとんどの命令は同期的に実行できる
    • LDRSTRのようなload-store操作は非同期であり、長い待ち時間を中心に命令実行を構成する必要がある
  • Fetcherは現在のプログラムカウンタの命令をプログラムメモリから非同期に取得する
  • Decoderは取得した命令をスレッド実行用の制御信号へデコードする
  • 各スレッドのレジスタファイルは計算中のデータを保持し、SIMDパターンを可能にする
    • 読み取り専用レジスタには%blockIdx%blockDim%threadIdxが入っている
    • カーネルはローカルスレッドIDに応じて異なるデータで実行できる
  • 各スレッドのALUはADDSUBMULDIV算術命令を処理する
  • CMPは2つのレジスタ差分の結果が負・0・正のどれかを出力し、その結果をPCユニットのNZPレジスタに保存する
  • 各スレッドのLSUはグローバルデータメモリにアクセスし、LDRSTR、および非同期メモリ待ち時間を処理する
  • 各スレッドのPCは次に実行する命令を決定する
    • デフォルトでは命令ごとに1ずつ増加する
    • BRnzpは直前のCMPが設定したNZPレジスタ条件が一致すると、特定のプログラムメモリ行へ分岐する
    • ループと条件文はこの方法で実装される
  • tiny-gpuは単純化のため、すべてのスレッドが各命令の後に同じPCへ収束すると仮定している
  • 実際のGPUでは個別スレッドが異なるPCへ分岐でき、このとき一緒に処理されていたスレッドグループが複数の実行フローへ分かれる分岐ダイバージェンスが発生する

ISA

  • tiny-gpuは行列加算や行列乗算のような概念実証用の単純なカーネルを実行するため、11命令のISAを実装している
  • 対応命令
    • BRnzp: NZP条件が一致したら別のプログラムメモリ行へジャンプ
    • CMP: 2つのレジスタ値を比較し、結果をNZPレジスタへ保存
    • ADD, SUB, MUL, DIV: テンソル演算向けの基本算術演算
    • LDR: グローバルメモリからデータをロード
    • STR: グローバルメモリへデータを保存
    • CONST: 定数値をレジスタへロード
    • RET: 現在のスレッド実行の終了を通知
  • 各レジスタは4ビットで指定され、合計16個のレジスタを持つ
    • R0からR12までの13個は読み書き可能な汎用レジスタ
    • 最後の3個はSIMDに必要な%blockIdx%blockDim%threadIdxを提供する読み取り専用特殊レジスタ

実行フロー

  • 各コアは命令実行時に次の段階の制御フローに従う
    • FETCH: 現在のPCの次の命令を取得
    • DECODE: 命令を制御信号へデコード
    • REQUEST: LDRまたはSTRが必要な場合、グローバルメモリへデータを要求
    • WAIT: 必要に応じてグローバルメモリ応答を待機
    • EXECUTE: データに対する計算を実行
    • UPDATE: レジスタファイルとNZPレジスタを更新
  • この制御フローは単純さと理解しやすさのために構成されている
  • 実際の実装では、一部段階を圧縮して処理時間を最適化したり、パイプライニングでコア資源上の複数命令実行を調整したりできる
  • 各スレッドは専用レジスタファイル内のデータを対象に同じ実行経路に沿って計算する
  • CPUのダイアグラムに似ているが、%blockIdx%blockDim%threadIdxが読み取り専用レジスタにあることでSIMD機能を可能にしている点が異なる

例示カーネル

  • ISAの概念実証として、行列加算と行列乗算カーネルが作成されている
  • リポジトリのテストファイルでは、これらのカーネルをGPU上で完全にシミュレーションし、データメモリ状態と全実行トレースを生成できる
  • 行列加算

    • matadd.asmは1 x 8行列2つを加算する
    • 8個の要素ごとの加算をそれぞれ別スレッドで実行する
    • %blockIdx%blockDim%threadIdxレジスタを使ってSIMDプログラミングを示す
    • LDRSTR命令を使い、非同期メモリ管理も含む
  • 行列乗算

    • matmul.asmは2 x 2行列2つを乗算する
    • 対応する行と列の内積を要素ごとに計算する
    • CMPBRnzpを使ってスレッド内部の分岐を示す
    • すべての分岐が再び収束するため、現在のtiny-gpu実装で動作する

シミュレーション

  • カーネルシミュレーションを実行するにはiverilogcocotbが必要
  • 準備手順
    • brew install icarus-verilogpip3 install cocotbでVerilogコンパイラとcocotbをインストール
    • sv2vの最新版をダウンロードして展開し、バイナリを$PATHに追加
    • リポジトリルートでmkdir buildを実行
  • カーネルシミュレーションはmake test_mataddmake test_matmulで実行する
  • 実行結果はtest/logsのログファイルとして出力される
    • 初期データメモリ状態
    • カーネルの全実行トレース
    • 最終データメモリ状態
  • 各ログファイルの冒頭には入力行列が表示され、末尾の最終データメモリには結果行列が表示される
  • 実行トレースには各サイクルごとの全コア・全スレッドの実行状態が含まれる
    • 現在の命令
    • PC
    • レジスタ値
    • 状態情報

意図的に省かれている高度なGPU機能

  • tiny-gpuは単純化のため、現代GPUの性能・機能向上要素の大半を除外している
  • 多層キャッシュと共有メモリ

    • 現代GPUはグローバルメモリアクセスを減らすために複数のキャッシュ階層を使う
    • tiny-gpuは要求元資源とメモリコントローラの間に最近のデータを保存する単一のキャッシュ階層だけを実装している
    • 多層キャッシュは頻繁に使うデータを使用箇所により近い場所へキャッシュしてロード時間を短縮する
    • GPUは同じblock内のスレッドが共有結果をやり取りできるよう、共有メモリを使うこともある
  • メモリコアレッシング

    • 並列実行中の複数スレッドは、行列の隣接要素のように連続したアドレスへアクセスすることが多い
    • メモリコアレッシングはキューに積まれたメモリ要求を分析し、隣接する要求を1つのトランザクションへ統合する
    • アドレッシングに使う時間を減らし、要求をまとめて処理することが目的である
  • パイプライニング

    • tiny-gpuのコアは、あるスレッド束の1命令の実行が終わってから次の命令を開始する
    • 現代GPUは依存関係のある命令の順次実行を保証しつつ、複数の逐次命令実行をストリーミングする
    • 非同期メモリ要求待ちのような状況でもコア資源が遊ばないようにし、資源利用率を高める
  • ワープスケジューリング

    • blockを一緒に実行可能なスレッドの束であるwarpに分ける
    • あるwarpが待機中のとき、別のwarpの命令を実行して単一コアで複数warpを同時に扱う
    • パイプライニングと似ているが、異なるスレッドの命令を扱う
  • 分岐ダイバージェンス

    • tiny-gpuは単一バッチ内のすべてのスレッドが各命令後に同じPCを持つと仮定している
    • 実際にはデータに応じて個別スレッドが別々の行へ分岐できる
    • 異なるPCを持つスレッドは別個の実行フローに分かれ、再収束する時点も管理しなければならない
  • 同期とバリア

    • 現代GPUでは、同じblock内のスレッドグループが特定地点に全員到達するまで待つようバリアを設定できる
    • スレッド同士で共有データを交換する必要がある場合、データ処理が完了したことを保証するのに有用である

次の作業

  • 今後の改善項目は次のとおり
    • 簡単な命令キャッシュの追加
    • Tiny Tapeout 7でGPUを使えるようにするアダプタの構築
    • 基本的な分岐ダイバージェンスの追加
    • 基本的なメモリコアレッシングの追加
    • 基本的なパイプライニングの追加
    • サイクル時間改善のための制御フローとレジスタ使用の最適化
    • グラフィックス機能を示すための基本的なグラフィックスカーネルの作成、または単純なグラフィックスハードウェアの追加
  • リポジトリの改善を望むユーザーはPRで貢献できる

1件のコメント

 
GN⁺ 2024-04-27
Hacker Newsのコメント
  • GPU市場は非常に競争が激しいため、現代アーキテクチャの低レベルな技術詳細は大半が非公開のままになっている
    例外的にIntelはGPU技術文書を数多く公開している: https://kiwitree.net/~lina/intel-gfx-docs/prm/
    i810/815のマニュアルもオンラインで見つかるし、855/910/915/945が抜けている965以前の奇妙な空白期間を除けば、文書化はかなり継続的に行われていた
  • 本当に素晴らしいプロジェクトで、こうしたハードウェアプロジェクトが公開で進められているのを見るのはうれしい
    ただ、これはSIMD補助プロセッサに近いと思う
    GPUと呼ぶには、少なくとも何らかの形のディスプレイ出力は必要だと考えている
    最近はNvidiaなどがサーバー専用のグラフィックアーキテクチャ派生品もGPUとして売っていて、用語がかなり緩くなっているのは分かるが、GPU設計においてグラフィックス部分はいまでも複雑さのかなりの部分を占めている
    • グラフィックスを処理するなら、出力がなくてもGPUと見なせると思う
      出力しないGPUにも依然として用途はある
      私の職場には中級クラスのQuadroを積んだワークステーションが75台ほどあるが、カードにはmini-DisplayPortしかなく、会社がHDMIケーブルしか買ってくれないので、すべて内蔵グラフィックスにつながれている
      それでもそれらのカードはソフトウェアを高速化し、グラフィックスを処理しており、単に画面出力をしていないだけだ
  • いいね。オープンコアGPUの取り組みは大いに応援したい
    別の例もある: https://github.com/jbush001/NyuziProcessor
    • こうしたオープンコアプロセッサのどれかに対して最小限のCUDA実装があるといいのに
      TSMCや他のファウンドリでこの種のプロセッサを経済的に生産するには、どの程度の数量が必要なのだろうか?
  • 本当に見事なプロジェクトだ
    FPGAをやってみたいのだが、正直どこから始めればいいのか見当もつかず、分野全体がかなり威圧的に感じられる
    最終目標はLLM向けのアクセラレータカードを作ることだが、完全に思いつきで決めた目標とはいえ、このプロジェクトと重なる部分は多そうで、おそらくより大きなモデルをロードするためのメモリオフロード部分くらいしか違わない気がする
    • 頭の中のフレームを切り替える必要がある
      FPGA入門は複数の下位技術に分解して考えるべきで、期待値も調整しないといけない
      ソフトウェアエンジニアに対して、最初から原理からコンピュータ全体を作り、命令セットアーキテクチャを書き、機械語を理解し、それをアセンブリに変換し、Pythonコードでアプリケーションを作るためにプログラミング言語まで開発しろとは普通期待しない
      上から始めてスタックを下っていくのが正しい
      複雑さを抽象化し、あらかじめ用意されたIPでシステムを構築することに集中すれば、FPGA設計はかなり簡単になる
      普通はMATLABのようなものを勧める。リファレンスデザインのあるDevKitでHDL Coderを使って初期アプリケーションを作れるからだ
      そうでなければ、デジタルコンピューティングアーキテクチャ、Verilog、タイミング、トランシーバー/入出力、ピン配置、Quartus/Vivado、シミュレーション/検証、組み込みシステムなどを学ぶという膨大な負担が生じる
      要するに、システムレベル設計から始めて、プラグアンドプレイのIPを持ってきてトップレベルで接続する方法を学び、そのモジュールをあらかじめ作られた基準設計に入れてみればよい
      その後、徐々に層をはがしながら下の複雑さを明らかにしていけばよい
    • 私も同じ立場で、計画はこうだ
      1. Harris, HarrisのDigital Design and Computer Architecture. (2022). Elsevierを読む: https://doi.org/10.1016/c2019-0-00213-0
      2. 著者のRVFpgaコースをたどり、FPGA上に実際のRISC-V CPUを作る: https://www.youtube.com/watch?v=ePv3xD3ZmnY
    • こういうルートを勧める
      1. 教育用リポジトリ https://github.com/yuri-panchul/basics-graphics-music をクローンする。Verilogを最初から学ぶ人向けの簡単な演習集で、GPU開発のためにImaginationで働いていたYuri Panchulが作成したものだ
      2. 対応する数十種類のFPGAボードのいずれかと、キーやLEDのようなアクセサリを入手する
      3. Yosysと関連ツールをインストールする
      4. lab01 DeMorganから始めて、リポジトリの演習をできるだけ多くこなす
        Harris&Harrisを読みながら演習を並行して進めることもできる
        演習と本を終えたら、自分のプロジェクトを始める段階になる
        ちなみにHackerMojoでは毎週の集まりもあり、ValleyにいなくてもZoomで参加できる
    • どの段階にいるかは分からないが、デジタル論理とCPU/GPUアーキテクチャをよりよく理解するのに、これらの資料が役立った
      1. https://learn.saylor.org/course/CS301
      2. https://www.coursera.org/learn/comparch
  1. https://hdlbits.01xz.net/wiki/Main_Page
  • LLMを高速化したいなら、まずアーキテクチャを理解する必要がある
    そこから始めればよい
    ハードウェアは実際には簡単な部分でもあり、製造面では難しい部分でもある
  • ここの順序 always ブロックで non-blocking 代入blocking 代入 演算子を混在させて使う理由はあるのか?
    • あれはローカル変数のように見える
    • シミュレーション結果と合成結果の一致にあまりこだわらないなら、そうしてもよい
  • ずっと前にVHDLで似たようなことをやったことがある
    opencores という、複数のオープンソースHDLプロジェクトが集まっていたサイトがあった
    最近、HPCレベルの大規模分散HDLシミュレータで良いものがあるのか気になる
    RTLレベルのシミュレーションに現代のGPUを活用するのは妥当に思える
  • ALUが DIV命令 をハードウェアレベルでそのまま実装するのか?
    現代のCUDAコアのようなものでは、実際に命令として除算を持つのが普通なのか、それとも普通はソフトウェアでエミュレーションするのか?
    実際のハードウェア除算回路は非常に面積を食うので、GPU ALUに入るとは予想していなかった
    Verilogでは DIV: begin alu_out_reg <= rs / rt; end と1行書くのはあまりにも簡単だが、その1行はシリコンをかなり消費する
    単にVerilogだけをシミュレーションしていると、その事実が見えないこともある
    • これは単に誰かの Verilog学習プロジェクト
      プロジェクトはシミュレーションで止まっていて、実際のハードウェアとして作るにははるかに多くの作業が必要だ
  • さらに、グラフィックス機能のない「GPU」でもある
    個人的には、こういうものは別の名前で呼ぶべきだと思う
    • 最初の問いは、そもそもなぜCPUとGPUが分かれたのか、ということだ
      両者の隔たりは縮まりつつあり、どちらも互いの機能を取り込みつつあるが、それでもなおかなりの違いがある
      私は アムダールの法則 と関係があると思う [0]
      その意味では、CPUはレイテンシ最適化プロセッサ、GPUはスループット最適化プロセッサと呼べる
      さらに具体的には、[1] CPUを長く深いデータ依存性のプロセッサ、GPUを広く浅いデータ依存性のプロセッサと呼ぶこともできる
      [0]: https://en.wikipedia.org/wiki/Amdahl%27s_law
      [1]: https://en.wikipedia.org/wiki/Data_dependency
    • TPU、つまり テンソル処理装置 と呼べる
      テンソルは単なるn次元配列だ
      その上にソフトウェアやファームウェアを載せて、GPUのように動作させることもできる
    • 「ディスプレイアダプタ」を作るプロジェクトを始めてみようかと考えてきたが、始める前から UEFIのGOPドライバ とディスプレイアダプタの間の通信プロトコルを把握できずに詰まっていた
      EDK2のソースからつなぎ合わせようとしたが、どこまでがQEMU特化なのか不明だ
    • MPU、つまり 行列処理装置 と呼べばよい
    • 定着しつつある用語はAIA、つまり AIアクセラレータ だと思う
  • tiny-gpu が、すべてのスレッドが各命令の後に同じプログラムカウンタへ「収束」すると仮定しているのは、あまりにも素朴な単純化だ
    実際のGPUでは個々のスレッドが異なるPCへ分岐でき、最初は一緒に処理されていたスレッド群が別々の実行へ分かれる 分岐発散 が生じる
    シリコンでGPUを作る前に、まずGPUプログラミングをやってみたほうがよかっただろう
    しかもSIMDと呼ぶにしても何かしっくりこない
    この人は以前、他人の回路をつなぎ合わせてLEDを点滅させ、CPUを作ったと言っていたあの人だ
    • 最初のものは、毎回の実行で __syncthreads() を呼ぶのと同じようなものではないか?