PYNQ-Z1 · 段階2 → 4 · 2026-09-08

壁は動かない

メモリの壁は 1.97 GB/s で、どう設計しても動きません。 だから道を広げるのをやめて、積荷のほうを小さくしました。 重みを三値にすると、同じ壁から 4.8 倍の重みが出てきます。 掛け算器は 1 個も使っていません。

追記(2026-09-13): そのあと「行列積以外」を実測したところ、 attention が CPU で 83 ms かかることが分かりました。7 節に足してあります。

01三段でやったこと

what the three stages measured

段階2で壁の位置を測り、段階3で壁まで計算できる回路を作り、 段階4で同じ壁から5倍を引き出しました。全部このボード1枚の上での実測です。

DDR から運んだバイト数 GB/s ・ どれもほぼ同じ=壁 壁 1.97 段階2 ・ 計算なし 1.97 段階3 ・ INT8 1.91 段階4 ・ 三値 1.83 0 1.0 2.0 取り出した重みの数 G重み/s ・ ここが 4.8 倍になった 段階3 ・ INT8 1.91 段階4 ・ 三値 9.16 4.8 倍 0 4 8
図1 / 運んだ量は変えていない。取り出した数を変えた。 上段はどれもほぼ同じで、これが動かない壁です。 変わったのは下段——同じ 1 バイトから何個の重みを取り出すか。 INT8 は 1 個、三値は 5 個。掛け算の速さでも演算器の数でもなく、 データの詰め方で 4.8 倍になりました。

02段階2 ・ 壁の位置を測る

where the wall actually is

最初にやったのは「どこまで速く読めるか」の確定です。ここは 別の資料(二つの天井)に 詳しく書いたので要点だけ。

天井は2つあります。DDR そのものの速さ(2.1 GB/s・買った時点で決まる)と、 PL 側の道幅(8 バイト × クロック・設計で変えられる)。 HP ポート1本では道幅が先に詰まって DDR の半分しか使えず、 2本にして初めて DDR 側が飽和しました。

このボードの本当の上限は 1.97 GB/s。 理論値 2.1 の 93.6% で、残りはリフレッシュなどの避けられない損失です。 ここから先は、この数字を前提に設計します。

03段階3 ・ 壁まで計算する

does computing cost bandwidth?

段階2で使ったのは受け取って捨てるだけの回路でした。 そこを本物の掛け算器に差し替えて、問いは一つだけ—— 計算させても 1.97 GB/s を保てるか。

保てない可能性は十分ありました。演算が追いつかなければ回路が上流を止め、 その瞬間に帯域が落ちるからです。だから設計上の絶対条件を一つ置きました。 受け入れ信号を常に 1 にする。上流を絶対に止めない。

入力ストリームの形 x(512 B) 先頭で取り込む 行1(512 B) 行2(512 B) 行3(512 B) …… 65,535 行 x を別の経路で配らずに済むので、AXI スレーブも BRAM 制御器も要らない DDR 重み AXI DMA 運搬役 MAC アレイ 掛け算 8 個/クロック 5 段パイプライン FIFO DDR 結果 8 B/クロック 4 B / 512 B 結果は読みの 0.8% しか出ない。書き戻しても帯域はほぼ食わない 実測 1.91 GB/s = 計算なしの 96.7%。131,070 行すべて numpy と一致
図2 / 捨てるだけの回路を、掛け算する回路に差し替えた。 入力ストリームの先頭 512 バイトを入力ベクトルとして取り込むのがこの設計の要点で、 おかげで x を配るための別経路(AXI スレーブや BRAM 制御器)が丸ごと要りません。 計算しても帯域は 96.7% 残りました。差の大半は結果の書き戻しで説明がつきます。

040.5 ns 足りなかった話

the first build did not meet timing

最初のビルドはタイミング未達で落ちました。ちょうど 0.5 ns 足りない。 FPGA では「1クロックの間に信号が届くか」が絶対条件で、届かなければ エラーも出ずに静かに間違った値が流れます。

0 2 4 6 ns 1クロック = 7.00 ns 修正前 1クロックに4つ カウンタ 配線 RAM LUT で組まれた掛け算(論理 9 段) 7.39 超過 修正後 2クロックに分割 ← 1クロック目:ここでレジスタに受ける DSP の掛け算 ← 2クロック目:LUT ではなく DSP に載せた +0.062
図3 / 原因は2つ重なっていた。 ①x の読み出しと掛け算を同じサイクルに詰め込んでいたこと。 ②8×8 の掛け算が「小さすぎる」と判断されて DSP に載らず、LUT で 9 段に組まれていたこと。 段を1つ足して分離し、use_dsp を指定して DSP に載せたら、 −0.502 ns → +0.062 ns になりました。 DSP は 220 個あるのに 0 個しか使われていなかった、というのが見落としの正体です。
この種の失敗は静かに起きる

タイミング未達のまま動かすこともできました。動きはします。ただし データが化けても何も言わずに間違った答えを返すだけです。 それでは「回路の論理が間違っているのか、タイミングなのか」を切り分けられなくなるので、 測定に進まず先に直しました。

05段階4 ・ 積荷を小さくする

three values per weight, five weights per byte

壁は動きません。ならば同じ 1 バイトからもっと多くの重みを取り出すしかない。 これが三値化(BitNet b1.58)です。重みを −1 / 0 / +1 の3通りに丸めてしまう。

3通りなら 1 個あたり log₂3 = 1.585 ビット。 実装上は 3⁵ = 243 ≤ 256 を使って、1 バイトにちょうど 5 個詰めます。 INT8 の 5 倍の密度です。

178 1 バイト 表引き 3 進数の各桁(178 = 1 + 2·3 + 1·9 + 0·27 + 2·81) 1 2 1 0 2 −1 桁 − 1 で 三値になる 重み(三値) 0 +1 0 −1 +1 入力ベクトル x x₀ x₁ x₂ x₃ x₄ 出てくる項 ── 掛け算ではなく 3 択 0 +x₁ 0 −x₃ +x₄ 加算木 40 項 → 1 個 「そのまま通す・ゼロにする・符号を反転する」——ここに掛け算器は 1 個も出てこない
図4 / 掛け算が 3 択に化ける。 重みが −1 / 0 / +1 しかないので、掛け算という演算がそもそも発生しません。 必要なのは選ぶことと符号を反転すること、そして足すことだけ。 これが「DSP ではなく LUT で作る」ことの中身です。 1 ビート 8 バイトから 40 個の項が出るので、40 項の加算木が要ります (手書きすると必ず間違えるので、生成スクリプトから書き出しました)。

06掛け算器が消えた

what it cost in silicon

段階3と段階4で、チップの資源をどう使ったかを並べます。 この比較がこの計画でいちばん言いたかったことです。

LUT 全 53,200 個 8,175 (15%) 9,557 (18%) +1,382 だけ DSP 全 220 個 20 (9%) 0 1 個も使わない 重みの流量 G重み/s 1.91 9.16 ■ 段階3 INT8  /  ■ 段階4 三値  ── LUT を 17% 増やして、掛け算器を全部手放して、4.8 倍
図5 / 少しの LUT と引き換えに、掛け算器を全部手放した。 220 個の DSP を持つチップで、本命の回路が DSP を 1 個も使っていません。 計画の最初に立てた「律速は帯域であって演算器の数ではない」という読みが、 最後まで正しかったということです。
段階演算器読み出し重みの流量DSPLUT照合
2 ・ 計算なし—1.97————
3 ・ INT8161.911.91208,175131,070 行
4 ・ 三値801.839.1609,557524,278 行

照合の欄は PL が出した答えを 1 行残らず numpy と突き合わせた結果です。 速いが壊れている、では意味がないので、全段階でこれをやりました。 実際、生成スクリプトのパイプライン段数が 1 つずれていたバグは、 この照合がなければ見つかりませんでした——動きはするが答えが全部違う、という形で出るからです。

07まだ出来ていないこと

what this is not, yet

この速度なら、135M パラメータを三値化したモデル(重み 27 MB)は 1 トークンあたり 14.7 ms = 約 68 tok/s という計算になります。 ただしこれは行列積だけの上限で、attention・layernorm・softmax・KV キャッシュは まだ何も作っていません。

そこで「残りがどれくらい重いのか」を実測しました(2026-09-13 追記)。 SmolLM2-135M の実寸法で、行列積以外を C で書いてボードの CPU で走らせた結果です。 予想より遥かに重く、計画の前提が変わりました。

1 トークンの時間の内訳(100% に正規化) 行列積(PL) attention(CPU) 小物(CPU) 文脈 128 26.2 tok/s 文脈 512 9.8 tok/s 文脈 2048 2.8 tok/s 行列積は文脈長に依存しない(つねに 14.68 ms)。attention だけが比例して増える
図6 / 速くした部分が、全体に占める割合の話。 緑がこの資料で作ってきた回路の担当ぶんです。 文脈が伸びるほど緑が痩せていきます——行列積は文脈長に依存しないのに、 attention は比例して増えるからです。文脈 2048 では PL の担当は 4%。 ここまで来ると、回路があってもほとんど効きません。
2026-09-13 追記 — 「行列積は PL、残りは CPU」は成立しない

文脈 512 で attention だけで 83 ms。行列積の 14.68 ms の 5.7 倍です。 系全体では 9.8 tok/s にしかならず、68 tok/s の 7 分の 1 になります。

実装が下手なわけではありません。素朴に float 変換する版は 158 ms でしたが、 INT8 のまま計算しても 83 ms で、実効 0.21 G MAC/s。 これは CPU の INT8 内積の実測上限 0.28 G MAC/s に近く、 Cortex-A9 の素の性能です。書き直しても縮みません。

やることは決まりました。attention も回路にします。 幸い、作るものは今あるエンジンとほぼ同じです——KV キャッシュは INT8 の別のストリームで、 QKT も AV も内積でしかありません。

サイズだけが違います。GQA で K/V の 3 ヘッドを 9 ヘッドで共有するので、 1 バイトあたり 3.15 回の再利用があります(行列積は 1 回でした)。 つまり MAC は 48 個欲しい——それでも DSP 220 個のうち 2 割ちょっとです。 softmax の exp も BRAM の表引きにすれば回路に載ります。

そこまでやるとどうなるか(試算)

attention が帯域律速になって文脈 512 で 2.94 ms。 予算は 14.68 + 2.94 + 4.2 ≒ 21.8 ms で 約 46 tok/s。 しかも 文脈が伸びても崩れにくくなります—— KV キャッシュの読み出しにも PL の帯域の優位がそのまま効くからです。

そのあとに残るのは、こういうものです。