Skip to content

Commit 01b46c4

Browse files
docs: measured scaling study — lane arrays, iso-chip budgets, clock cost
docs/scaling.md answers 'what does parallel instantiation buy?' with measurements from a new probe, fpga/scale_sweep.sh (butterfly lane arrays, each lane with its own twiddle ROM, register-wrapped like fmax.sh with a two-stage XOR reduce so the harness never owns the critical path). Measured: - Area is linear d=16..240 and K-RED's LUT premium is ~zero: a kred lane (butterfly + psi-fold ROM) is a few percent CHEAPER in LUTs than a ref lane (Barrett + full ROM) at one third the DSPs — the psi-fold ROM pays for the K-RED adders. The remaining trade is ~7x CARRY4. - At equal lane count the clock gap is small: ref 74.1 vs kred 69.2 MHz at d=32 (module-level it was -26%, core-level ~1%); kred degrades gracefully to 67.8 MHz at d=64. - Operational finding: ref at d=64 = 80% DSP utilization never finished nextpnr placement in 15 h (dense DSP placement is the open placer's weak spot); the same 64 lanes are a routine 27%-DSP build with K-RED. kred d=128 (83% LUT) still routing; appended when done. Analysis grounded on those numbers: iso-chip lane budgets (the retrofit converts the binding resource from DSP to LUT; 1.0-1.5x lanes on standalone Artix parts plus half-to-two-thirds of DSPs freed; the full 3x materializes when DSPs are contended), xc7a100t throughput shape (~1.4x NTT/s with 120 DSPs spare), and the ceiling story (stage width 512; full 10-stage unroll = 5,120 DSPs vs 15,360 — one fits the largest current devices, the other does not). All caveats stated: no banked memory network in the probe, one seed, open-flow timing, farm absolutes below isolated-module Fmax by design. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 094a9a1 commit 01b46c4

3 files changed

Lines changed: 256 additions & 0 deletions

File tree

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ claim traces to a CI-reproducible script.
1212
| `evaluation.md` | system-sim results + synthesis cost table (PnR TODO) |
1313
| `generalization.md` | Proth-prime generator; Falcon + Kyber instances |
1414
| `algorithm-level.md` | where the ideas apply outside hardware; prime-selection guidance (with `generator/prime_scout.py`) |
15+
| `scaling.md` | measured lane-array scaling: iso-chip parallelism, clock cost, the ceiling (with `fpga/scale_sweep.sh`) |
1516
| `paper/paper.md` | full manuscript draft (section TODOs + refs inline) |
1617
| `venue-assessment.md` | where-to-submit / what-level analysis |
1718

docs/scaling.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Scaling: what the 3→1 DSP saving buys under parallel instantiation
2+
3+
The paper's single-BFU core is deliberately minimal. This note asks the
4+
next question: if you spend the saving on parallel butterfly lanes, how
5+
much performance does one chip buy, and where is the ceiling? Numbers
6+
here are measured with `fpga/scale_sweep.sh` unless marked as estimates;
7+
none of this is in the paper's claim set.
8+
9+
## Method
10+
11+
`fpga/scale_sweep.sh` synthesizes a *lane array*: d butterflies, each
12+
with its own twiddle ROM, fed from a shift register, outputs reduced
13+
through a two-stage registered XOR so the harness never owns the
14+
critical path (an earlier single-stage XOR tree did, and cost ~20 MHz —
15+
kept in the git history as a cautionary tale). Two variants:
16+
17+
- **ref**: reference `compact_bf` (Barrett, 3 DSP) + full `tf_ROM`
18+
- **kred**: `compact_bf_v2` (K-RED, 1 DSP) + ψ-fold `tf_rom_fold`
19+
20+
The probe measures how the *arithmetic* scales. It deliberately excludes
21+
the banked memory system, address generators and lane-to-bank
22+
permutation network a full d-lane core needs; those add area to both
23+
variants and their wiring pressure grows with d.
24+
25+
## Measured: area scales linearly, and the LUT cost per lane is a wash
26+
27+
yosys `synth_xilinx` (Artix-7 primitives):
28+
29+
| variant | d | LUT | FF | DSP | CARRY4 |
30+
|---|---|---|---|---|---|
31+
| kred | 16 | 6,876 | 2,946 | 16 | 960 |
32+
| kred | 64 | 27,265 | 11,778 | 64 | 3,840 |
33+
| kred | 128 | 52,870 | 23,554 | 128 | 7,680 |
34+
| kred | 240 | 98,398 | 44,162 | 240 | 14,400 |
35+
| ref | 16 | 6,917 | 2,898 | 48 | 384 |
36+
| ref | 64 | 27,785 | 11,586 | 192 | 1,536 |
37+
| ref | 80 | 34,579 | 14,482 | 240 | 1,920 |
38+
39+
Two facts fall out:
40+
41+
1. **Linear**: per-lane cost is essentially constant from d=16 to d=240
42+
(kred 410–430 LUT/lane, ref ~432 LUT/lane); no superlinear glue in
43+
the arithmetic itself.
44+
2. **The LUT premium of K-RED is zero — slightly negative.** The
45+
butterfly alone costs more LUTs than Barrett's (231 vs 158), but the
46+
ψ-fold ROM gives back more than the difference (192 vs 241): a full
47+
kred lane is a few percent *cheaper* in LUTs than a ref lane, at one
48+
third the DSPs. The 3→1 DSP saving is not bought with fabric. The
49+
trade that remains: kred lanes carry ~7× the CARRY4 chains
50+
(shift-add adders), which is where routing pressure would appear.
51+
52+
## Measured: post-route clock at equal lane count
53+
54+
openXC7 nextpnr-xilinx, xc7a100t, one seed, 2.0 ns target:
55+
56+
| variant | d | DSP util | LUT util | post-route Fmax |
57+
|---|---|---|---|---|
58+
| kred | 32 | 13% | 22% | 69.2 MHz |
59+
| ref | 32 | 40% | 22% | 74.1 MHz |
60+
| kred | 64 | 27% | 43% | 67.8 MHz |
61+
| kred | 128 | 53% | 83% | (routing at the time of writing; appended below when done) |
62+
| ref | 64 | 80% | 44% | **did not converge**: 15 h of placement without a result |
63+
64+
Three observations:
65+
66+
- **At equal lane count, Barrett clocks ~7% faster** (74.1 vs
67+
69.2 MHz at d=32). The module-level gap was −26% in isolation; in a
68+
lane array it shrinks to ~7%, and in the full single-BFU core it was
69+
~1%. So the clock price of K-RED depends on how much else surrounds
70+
the butterfly, and at farm scale it is small against the 3× DSP
71+
saving.
72+
- **kred's congestion behaviour is graceful so far**: 69.2 → 67.8 MHz
73+
(−2%) from d=32 to d=64 despite the CARRY4 density.
74+
- **Dense DSP placement is the open flow's weak spot**: ref at d=64
75+
(192 of 240 DSPs, 80%) never finished placement in 15 hours. That is
76+
a statement about nextpnr-xilinx's placer, not about the Barrett
77+
design, but it is operationally real if the open flow is your
78+
toolchain: with K-RED the same 64 lanes are a routine 27%-DSP build.
79+
80+
Absolute farm frequencies (~68–74 MHz) sit below the isolated-module
81+
Fmax (~122–164 MHz) because the harness loads every lane from one giant
82+
shift register with a shared select; use the *relative* numbers.
83+
84+
## Iso-chip lane budgets
85+
86+
With measured per-lane costs, the lane count a device supports is
87+
`min(DSP budget, usable-LUT budget)`; we assume 80% of LUTs are usable
88+
by lanes (the rest for the memory system this probe excludes):
89+
90+
| device | LUT / DSP | ref lanes (DSP-bound) | kred lanes | lane ratio | DSPs left over (kred) |
91+
|---|---|---|---|---|---|
92+
| xc7a35t | 20.8k / 90 | 30 | ~39 (LUT-bound) | 1.3× | 51 |
93+
| xc7a100t | 63.4k / 240 | 80 | ~120 (LUT-bound) | 1.5× | 120 |
94+
| xc7a200t | 134.6k / 740 | ~246 | ~254 (LUT-bound) | ~1.0× | ~486 |
95+
96+
The honest reading: **the retrofit converts the binding resource from
97+
DSP to LUT.** On a standalone Artix part the lane-count gain is
98+
1.0–1.5×, *and* half to two-thirds of the device's DSPs stay free. The
99+
full 3× materializes exactly when DSPs are the contended resource — a
100+
DSP-poor device, or the realistic case where the NTT shares the chip
101+
with other DSP-hungry logic (an FHE accelerator's multipliers, filters,
102+
anything). In every case the freed DSPs are the fungible currency; the
103+
lane ratio is what a dedicated-chip benchmark would see.
104+
105+
Putting lanes and clocks together for xc7a100t (5,120 butterflies per
106+
1024-point NTT, 1 butterfly/lane/cycle; farm clocks, so shapes not
107+
absolutes): ref at 80 lanes and ~74 MHz gives 64 cycles ≈ 0.86 µs/NTT;
108+
kred at 120 lanes and ~68 MHz gives ~43 cycles ≈ 0.63 µs/NTT — about
109+
**1.4× the transforms per second per chip, with 120 DSPs still free**.
110+
111+
## The ceiling
112+
113+
Parallelism inside one transform is capped by the stage width:
114+
N/2 = 512 butterflies for N = 1024. Beyond that, unroll the 10 stages
115+
into a pipeline (one transform per cycle in steady state): 5,120
116+
butterflies in flight, i.e. **5,120 DSPs for K-RED vs 15,360 for
117+
Barrett**. The largest current devices carry ~14k DSPs, so the fully
118+
unrolled Falcon NTT fits on one chip with K-RED and does not with
119+
Barrett (estimate from device datasheets, not synthesized). That is the
120+
general shape of the answer: every scaling ceiling — stage width, chip
121+
DSP budget, board count — sits 3× further away in the resource NTT
122+
accelerators are usually bound by.
123+
124+
## Reproduce
125+
126+
```sh
127+
# area sweep (minutes)
128+
nix shell nixpkgs#yosys --command fpga/scale_sweep.sh
129+
# post-route Fmax points (tens of minutes each; PNR_SET picks points)
130+
DO_PNR=1 NP=<nextpnr-xilinx> CHIPDB=<xc7a100tcsg324.bin> fpga/scale_sweep.sh
131+
```
132+
133+
Caveats: lane arrays without the banked memory network (its wiring cost
134+
grows with d and hits both variants); one PnR seed; open-flow timing
135+
model (see the paper §6's hedge); the 80%-usable-LUT assumption; ROMs
136+
map to distributed LUT-ROM at this size, so BRAM counts are zero in the
137+
probe.

fpga/scale_sweep.sh

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#!/usr/bin/env bash
2+
# Butterfly-farm scaling probe: how do LUT/FF/DSP scale when d butterfly
3+
# lanes (each with its own twiddle ROM) are instantiated side by side, for
4+
# the reference (Barrett, 3 DSP/lane, full tf_ROM) vs the proposed
5+
# (K-RED, 1 DSP/lane, psi-fold tf_rom_fold)? Backs docs/scaling.md.
6+
#
7+
# This is a LANE-ARRAY probe, deliberately without the banked memory
8+
# network / AGU of a full d-lane core: it measures how the arithmetic
9+
# scales and where each variant hits the device's DSP/LUT wall.
10+
#
11+
# Area sweep (yosys synth_xilinx):
12+
# nix shell nixpkgs#yosys --command fpga/scale_sweep.sh
13+
# Optional post-route Fmax points (openXC7; slow, ~10-40 min each):
14+
# DO_PNR=1 NP=<nextpnr-xilinx> CHIPDB=<xc7a100tcsg324.bin> fpga/scale_sweep.sh
15+
set -euo pipefail
16+
cd "$(dirname "$0")/.."
17+
RTL=cfntt_ref/hardware_code_radix-2
18+
YOSYS=${YOSYS:-yosys}
19+
OUT=${OUT:-/tmp/scale_sweep}; mkdir -p "$OUT"
20+
21+
KRED_SRC="kred-butterfly/compact_bf_v2.v kred-butterfly/modular_mul_kred.v \
22+
psi-fold-rom/tf_rom_fold.v $RTL/modular_add.v $RTL/modular_substraction.v \
23+
$RTL/modular_half.v $RTL/common_lib.v"
24+
REF_SRC="$RTL/compact_bf.v $RTL/modular_mul.v $RTL/tf_ROM.v \
25+
$RTL/modular_add.v $RTL/modular_substraction.v $RTL/common_lib.v"
26+
27+
gen_farm() { # kind d -> $OUT/farm_<kind>_<d>.v (self-wrapped: clk/serial io)
28+
local kind=$1 d=$2 bf rom
29+
if [ "$kind" = kred ]; then bf=compact_bf_v2; rom=tf_rom_fold
30+
else bf=compact_bf; rom=tf_ROM; fi
31+
python3 - "$kind" "$d" "$bf" "$rom" > "$OUT/farm_${kind}_${d}.v" <<'PY'
32+
import sys
33+
kind, d, bf, rom = sys.argv[1], int(sys.argv[2]), sys.argv[3], sys.argv[4]
34+
IB = 38 * d + 1 # per lane: u14 v14 addr10; +1 shared sel
35+
OB = 28 * d
36+
print(f"// GENERATED by scale_sweep.sh: {d}-lane {kind} butterfly farm")
37+
print(f"module farm (input clk, input serial_in, output serial_out);")
38+
print(f" reg [{IB-1}:0] ish;")
39+
print(f" always @(posedge clk) ish <= {{ish[{IB-2}:0], serial_in}};")
40+
print(f" wire sel = ish[{IB-1}];")
41+
print(f" wire [{OB-1}:0] ow;")
42+
for i in range(d):
43+
b = 38 * i
44+
print(f" wire [13:0] w{i};")
45+
print(f" {rom} rom{i} (.clk(clk), .A(ish[{b+37}:{b+28}]), .REN(1'b1), .Q(w{i}));")
46+
print(f" {bf} bf{i} (.clk(clk), .rst(1'b0), .sel(sel),")
47+
print(f" .u(ish[{b+27}:{b+14}]), .v(ish[{b+13}:{b}]), .w(w{i}),")
48+
print(f" .bf_upper(ow[{28*i+27}:{28*i+14}]), .bf_lower(ow[{28*i+13}:{28*i}]));")
49+
print(f" reg [{OB-1}:0] osh;")
50+
print(f" reg [{d-1}:0] xr1;")
51+
print(f" reg xr2;")
52+
print(f" always @(posedge clk) begin")
53+
print(f" osh <= ow;")
54+
# pipeline the XOR reduce so the harness never owns the critical path:
55+
# per-lane 28-bit XOR, then a registered d-bit XOR, then the pin
56+
for i in range(d):
57+
print(f" xr1[{i}] <= ^osh[{28*i+27}:{28*i}];")
58+
print(f" xr2 <= ^xr1;")
59+
print(f" end")
60+
print(f" assign serial_out = xr2;")
61+
print(f"endmodule")
62+
PY
63+
}
64+
65+
synth_stat() { # kind d srcs...
66+
local kind=$1 d=$2; shift 2
67+
gen_farm "$kind" "$d"
68+
local reads="" tmp="$OUT/stat_${kind}_${d}.txt"
69+
for f in "$@" "$OUT/farm_${kind}_${d}.v"; do reads="$reads read_verilog $f;"; done
70+
$YOSYS -p "$reads synth_xilinx -flatten -top farm; tee -o $tmp stat" >/dev/null 2>&1
71+
awk -v K="$kind" -v D="$d" '
72+
/^ *[0-9]+ +LUT[1-6]/ {lut+=$1}
73+
/^ *[0-9]+ +FD(CE|RE|PE|SE)/ {ff+=$1}
74+
/^ *[0-9]+ +DSP48E1/ {dsp+=$1}
75+
/^ *[0-9]+ +RAMB(18|36)E1/ {bram+=$1}
76+
/^ *[0-9]+ +CARRY4/ {carry+=$1}
77+
END {printf "%-6s d=%-4d LUT=%-6d FF=%-6d DSP=%-4d BRAM=%-3d CARRY4=%-5d\n",
78+
K, D, lut, ff, dsp, bram, carry}
79+
' "$tmp"
80+
}
81+
82+
echo "== area sweep (yosys synth_xilinx, Artix-7 primitives) =="
83+
for d in 16 64 128 240; do synth_stat kred $d $KRED_SRC; done
84+
for d in 16 64 80; do synth_stat ref $d $REF_SRC; done
85+
86+
if [ "${DO_PNR:-0}" = 1 ]; then
87+
: "${NP:?}" "${CHIPDB:?}"
88+
# Which points to place-and-route, as "kind:d" pairs. Note ref:64
89+
# (192/240 DSP = 80% DSP utilization) did NOT converge in 15 h of
90+
# nextpnr placement in our runs — dense DSP placement is hard for the
91+
# open placer — hence ref is probed at d=32.
92+
PNR_SET=${PNR_SET:-"kred:32 ref:32 kred:64 kred:128"}
93+
echo "== post-route Fmax points (xc7a100t, 1 seed, target 2.0 ns) =="
94+
pnr() { # kind d srcs...
95+
local kind=$1 d=$2; shift 2
96+
gen_farm "$kind" "$d"
97+
local reads=""
98+
for f in "$@" "$OUT/farm_${kind}_${d}.v"; do reads="$reads read_verilog $f;"; done
99+
$YOSYS -p "$reads synth_xilinx -flatten -top farm; delete t:\$scopeinfo; \
100+
write_json $OUT/farm_${kind}_${d}.json" >/dev/null 2>&1
101+
{ echo "set_property IOSTANDARD LVCMOS33 [get_ports {clk}]"
102+
echo "set_property IOSTANDARD LVCMOS33 [get_ports {serial_in}]"
103+
echo "set_property IOSTANDARD LVCMOS33 [get_ports {serial_out}]"
104+
echo "create_clock -period 2.0 -name clk [get_ports clk]"; } > "$OUT/c.xdc"
105+
local f log="$OUT/pnr_${kind}_${d}.log"
106+
"$NP" --chipdb "$CHIPDB" --seed 1 --xdc "$OUT/c.xdc" \
107+
--json "$OUT/farm_${kind}_${d}.json" > "$log" 2>&1 || true
108+
f=$(grep -oE "Max frequency for clock.*: [0-9.]+ MHz" "$log" \
109+
| grep -oE "[0-9.]+" | tail -1 || echo FAIL)
110+
printf "%-6s d=%-4d Fmax=%s MHz (log: %s)\n" "$kind" "$d" "${f:-FAIL}" "$log"
111+
}
112+
for pt in $PNR_SET; do
113+
case $pt in
114+
kred:*) pnr kred "${pt#kred:}" $KRED_SRC ;;
115+
ref:*) pnr ref "${pt#ref:}" $REF_SRC ;;
116+
esac
117+
done
118+
fi

0 commit comments

Comments
 (0)