メモリの壁は 1.97 GB/s で、どう設計しても動きません。 だから道を広げるのをやめて、積荷のほうを小さくしました。 重みを三値にすると、同じ壁から 4.8 倍の重みが出てきます。 掛け算器は 1 個も使っていません。
追記(2026-09-13): そのあと「行列積以外」を実測したところ、 attention が CPU で 83 ms かかることが分かりました。7 節に足してあります。
what the three stages measured
段階2で壁の位置を測り、段階3で壁まで計算できる回路を作り、 段階4で同じ壁から5倍を引き出しました。全部このボード1枚の上での実測です。
where the wall actually is
最初にやったのは「どこまで速く読めるか」の確定です。ここは 別の資料(二つの天井)に 詳しく書いたので要点だけ。
天井は2つあります。DDR そのものの速さ(2.1 GB/s・買った時点で決まる)と、 PL 側の道幅(8 バイト × クロック・設計で変えられる)。 HP ポート1本では道幅が先に詰まって DDR の半分しか使えず、 2本にして初めて DDR 側が飽和しました。
does computing cost bandwidth?
段階2で使ったのは受け取って捨てるだけの回路でした。 そこを本物の掛け算器に差し替えて、問いは一つだけ—— 計算させても 1.97 GB/s を保てるか。
保てない可能性は十分ありました。演算が追いつかなければ回路が上流を止め、 その瞬間に帯域が落ちるからです。だから設計上の絶対条件を一つ置きました。 受け入れ信号を常に 1 にする。上流を絶対に止めない。
the first build did not meet timing
最初のビルドはタイミング未達で落ちました。ちょうど 0.5 ns 足りない。 FPGA では「1クロックの間に信号が届くか」が絶対条件で、届かなければ エラーも出ずに静かに間違った値が流れます。
use_dsp を指定して DSP に載せたら、
−0.502 ns → +0.062 ns になりました。
DSP は 220 個あるのに 0 個しか使われていなかった、というのが見落としの正体です。
タイミング未達のまま動かすこともできました。動きはします。ただし データが化けても何も言わずに間違った答えを返すだけです。 それでは「回路の論理が間違っているのか、タイミングなのか」を切り分けられなくなるので、 測定に進まず先に直しました。
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 倍の密度です。
what it cost in silicon
段階3と段階4で、チップの資源をどう使ったかを並べます。 この比較がこの計画でいちばん言いたかったことです。
| 段階 | 演算器 | 読み出し | 重みの流量 | DSP | LUT | 照合 |
|---|---|---|---|---|---|---|
| 2 ・ 計算なし | — | 1.97 | — | — | — | — |
| 3 ・ INT8 | 16 | 1.91 | 1.91 | 20 | 8,175 | 131,070 行 |
| 4 ・ 三値 | 80 | 1.83 | 9.16 | 0 | 9,557 | 524,278 行 |
照合の欄は PL が出した答えを 1 行残らず numpy と突き合わせた結果です。 速いが壊れている、では意味がないので、全段階でこれをやりました。 実際、生成スクリプトのパイプライン段数が 1 つずれていたバグは、 この照合がなければ見つかりませんでした——動きはするが答えが全部違う、という形で出るからです。
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 で走らせた結果です。 予想より遥かに重く、計画の前提が変わりました。
文脈 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 の帯域の優位がそのまま効くからです。
そのあとに残るのは、こういうものです。