Skip to main content

logicaffeine_compile/spatial/
fpga_decode.rs

1//! A decode on one ZCU104: what the board holds, how fast its programmable logic reads it, and what a token of
2//! each checkpoint we have costs there.
3//!
4//! The ZCU104's XCZU7EV keeps 4.98 MB in its block RAMs and UltraRAMs, and reaches the board's 2 GB of DDR4 only
5//! through the processing system: the programmable logic reads it over 128-bit AXI HP ports, each moving 16 bytes
6//! a PL clock cycle, all behind one DDR4-2400 controller on a 64-bit bus. So the board's decode rate is set by the
7//! PL clock and how many ports the fabric drives, an operating point rather than one number, and
8//! [`zcu104_points`] names the three that bound it: four ports at 100 MHz, which the FPT'19 study measured
9//! saturating on the ZCU102 (the same processing system, the same DDR4 rate and width); three ports at 300 MHz, its
10//! best measured read at 75% of the DDR's peak; and the DDR's peak itself.
11//!
12//! [`zcu104_sizing`] prices every local checkpoint at each point with [`price`]: the floor no implementation on
13//! the board can beat, where the weights live, and the term that binds.
14
15use std::path::Path;
16
17use serde::Serialize;
18
19use super::decode::{price, Bind, Chip, DecodeRefusal, DecodeStep, Deployment, GgufHeader, Machine, Tier, TierKind};
20use super::part::{PartResources, DS891, UG573};
21use super::rdu_chip::{Fact, Provenance};
22use crate::codegen_sva::fabric_target::Precision;
23
24pub const UG1267: &str = "UG1267 (v1.1) ZCU104 Evaluation Board User Guide";
25pub const FPT19: &str = "pure.manchester.ac.uk/ws/files/137731499/FPT19_paper_14.pdf";
26pub const WP486: &str = "WP486 (v1.0.1) Deep Learning with INT8 Optimization on Xilinx Devices";
27
28const ZCU104_SOURCES: &[&str] = &[DS891, UG573, UG1267, FPT19, WP486];
29
30/// Bytes one 128-bit AXI HP port carries in a PL clock cycle.
31pub const HP_PORT_BYTES: f64 = 16.0;
32
33/// The DDR4 behind the processing system, as the board guide states it.
34pub fn ddr_capacity() -> Fact<f64> {
35    Fact::derived(
36        2_147_483_648.0,
37        "UG1267 (v1.1) ZCU104 Evaluation Board User Guide: \"PS-side memory is a 2 GB, 64-bit wide DDR4 memory system comprised \
38         of four 256 Mb x 16 SDRAMs\", each \"4 Gb (256 Mb x 16)\": 4 × 2^32 / 8 bytes",
39    )
40}
41
42/// What the DDR4 itself moves at most: its transfer rate times its bus width.
43pub fn ddr_peak() -> Fact<f64> {
44    Fact::derived(
45        19.2e9,
46        "UG1267 (v1.1) ZCU104 Evaluation Board User Guide: \"Micron MT40A256M16GE-083E (DDR4-2400)\", \"64-bit wide\": 2400 MT/s \
47         × 8 bytes",
48    )
49}
50
51/// One way the programmable logic can run against the DDR: its clock, the HP ports it reads through, and what
52/// those ports read.
53#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
54pub struct PlPoint {
55    /// The chip's name at this point.
56    pub name: &'static str,
57    pub pl_hz: Fact<f64>,
58    pub hp_ports: u32,
59    /// Bytes per second the fabric reads from the DDR through those ports.
60    pub ddr_read: Fact<f64>,
61}
62
63/// The operating points that bound a ZCU104 decode, slowest first. The first is the catalog's `zcu104`.
64pub fn zcu104_points() -> Vec<PlPoint> {
65    vec![
66        PlPoint {
67            name: "zcu104",
68            pl_hz: Fact::modeled(
69                100e6,
70                "not published for a ZCU104 image of ours: the 100MHz PL clock the FPT'19 study ran its four-port read at, \
71                 inherited until a routed image measures one",
72            ),
73            hp_ports: 4,
74            ddr_read: Fact::derived(
75                6.4e9,
76                "pure.manchester.ac.uk/ws/files/137731499/FPT19_paper_14.pdf (FPT 2019, \"Unexpected Diversity: Quantitative \
77                 Memory Analysis for Zynq UltraScale+ Systems\"): on the ZCU102, whose DDR4 is \"2400 MT/s × 64-bit\" as the \
78                 ZCU104's is (UG1267 (v1.1)), at 100MHz the four-port configuration \"achieves 6.4 GB/s read throughput\" as \
79                 \"all 4 AXI read ports achieve their theoretical maximum of 1.6 GB/s\"",
80            ),
81        },
82        PlPoint {
83            name: "zcu104-pl300",
84            pl_hz: Fact::modeled(
85                300e6,
86                "not published for a ZCU104 image of ours: the 300MHz PL clock of the FPT'19 study's best read, inherited until \
87                 a routed image measures one",
88            ),
89            hp_ports: 3,
90            ddr_read: Fact::derived(
91                14.4e9,
92                "pure.manchester.ac.uk/ws/files/137731499/FPT19_paper_14.pdf (FPT 2019): the ZCU102's peak, HP0, HP1 and HP3 at \
93                 300MHz with 128 Byte bursts, \"is 75% of the theoretical peak for the DDR memory\": 0.75 × 19.2 GB/s, the \
94                 DDR4-2400 × 64-bit the ZCU104 shares (UG1267 (v1.1))",
95            ),
96        },
97        PlPoint {
98            name: "zcu104-ddr-peak",
99            pl_hz: Fact::modeled(
100                300e6,
101                "not published for a ZCU104 image of ours: the slowest PL clock at which four 16-byte HP ports reach the DDR's \
102                 19.2 GB/s, inherited from that bound until a routed image measures one",
103            ),
104            hp_ports: 4,
105            ddr_read: ddr_peak(),
106        },
107    ]
108}
109
110/// The ZCU104 at its four-port, 100 MHz point: the one the catalog prices.
111pub fn zcu104() -> Chip {
112    zcu104_at(&zcu104_points()[0])
113}
114
115/// The ZCU104 at `p`. Its SRAM is the part's block RAMs and UltraRAMs, read on every port at the PL clock; its DDR
116/// is the board's, read at what the point's ports read; its matrix rate is two int8 multiply-accumulates per DSP
117/// slice per PL cycle. Facts computed from the clock are as strong as the clock.
118pub fn zcu104_at(p: &PlPoint) -> Chip {
119    let part = PartResources::xczu7ev();
120    let at_clock = |value: f64, cite: &'static str| Fact { value, provenance: Provenance::Derived.max(p.pl_hz.provenance), cite };
121    Chip {
122        name: p.name,
123        tiers: vec![
124            Tier {
125                kind: TierKind::Sram,
126                capacity: Fact::derived(
127                    part.block_bytes() as f64,
128                    "DS891 (v1.11.1) Zynq UltraScale+ MPSoC Data Sheet: Overview, Table 5: ZU7EV \"Block RAM Blocks 312\", \
129                     \"UltraRAM Blocks 96\"; UG573 (v1.9) UltraScale Architecture Memory Resources: 512 x 72 and 4K x 72 bits \
130                     each: 312 × 4,608 + 96 × 36,864 bytes",
131                ),
132                bandwidth: at_clock(
133                    part.block_read_bytes_per_cycle() as f64 * p.pl_hz.value,
134                    "not published as a rate: DS891 (v1.11.1) Table 5's 312 block RAMs and 96 UltraRAMs read at UG573 (v1.9)'s \
135                     port widths, 72 and 2 × 72 bits a cycle, times the point's PL clock, which no ZCU104 image of ours has \
136                     measured yet",
137                ),
138            },
139            Tier { kind: TierKind::Ddr, capacity: ddr_capacity(), bandwidth: p.ddr_read },
140        ],
141        matrix: vec![(
142            Precision::Int8,
143            at_clock(
144                part.dsp.value as f64 * 2.0 * 2.0 * p.pl_hz.value,
145                "not published as a rate: DS891 (v1.11.1) Table 5's \"DSP Slices 1,728\", each processing \"two concurrent INT8 \
146                 multiply and accumulate (MACC) operations\" (WP486 (v1.0.1) Deep Learning with INT8 Optimization on Xilinx \
147                 Devices), two operations each, at the point's PL clock, which no ZCU104 image of ours has measured yet",
148            ),
149        )],
150        stage_overhead: None,
151        link: None,
152        power: None,
153        sources: ZCU104_SOURCES,
154    }
155}
156
157// ── The sizing ──────────────────────────────────────────────────────────────────────────────────────
158
159/// The checkpoints the sizing prices: every local one, in `benchmarks/silicon/decode/headers/`.
160pub const ZCU104_MODELS: &[&str] = &["qwen3.5-0.8b-q8_0", "gemma-3-1b-it-q8_0", "llama-3.2-1b-instruct-q8_0", "qwen3-8b-q8_0"];
161
162/// The context the sizing's KV caches have reached.
163pub const ZCU104_CTX: u64 = 512;
164
165/// One operating point, as the sizing states it.
166#[derive(Clone, Debug, PartialEq, Serialize)]
167pub struct PointSummary {
168    pub name: &'static str,
169    pub pl_mhz: f64,
170    pub pl_provenance: Provenance,
171    pub hp_ports: u32,
172    pub ddr_read_gb_s: f64,
173    pub ddr_read_provenance: Provenance,
174    pub sram_read_gb_s: f64,
175    pub int8_tops: f64,
176}
177
178/// A checkpoint at one operating point.
179#[derive(Clone, Debug, PartialEq, Serialize)]
180pub struct SizingRow {
181    pub point: &'static str,
182    /// No decode on the board at this point is faster.
183    pub floor_tok_s: f64,
184    pub binds: Bind,
185    /// Of the held bytes, the share the on-chip RAM keeps.
186    pub on_chip_share: f64,
187    pub provenance: Provenance,
188}
189
190/// Whether a checkpoint fits the board.
191#[derive(Clone, Debug, PartialEq, Serialize)]
192pub enum Fit {
193    Resident { rows: Vec<SizingRow> },
194    NotResident { needed: f64, capacity: f64 },
195}
196
197/// One checkpoint on the board.
198#[derive(Clone, Debug, PartialEq, Serialize)]
199pub struct ModelSizing {
200    pub model: String,
201    pub file_bytes: u64,
202    /// Whether its head reads the embedding table (no `output.weight`).
203    pub tied_head: bool,
204    pub held_bytes: f64,
205    pub weight_bytes_per_token: f64,
206    pub state_bytes_per_token: f64,
207    pub fit: Fit,
208}
209
210/// Every local checkpoint, priced on the ZCU104 at each operating point.
211#[derive(Clone, Debug, PartialEq, Serialize)]
212pub struct Zcu104Sizing {
213    pub board: &'static str,
214    pub part: &'static str,
215    pub ctx: u64,
216    pub on_chip_bytes: f64,
217    pub ddr_bytes: f64,
218    pub points: Vec<PointSummary>,
219    pub models: Vec<ModelSizing>,
220}
221
222/// The sizing, read from the header ledgers in `decode_dir`.
223pub fn zcu104_sizing(decode_dir: &Path) -> Result<Zcu104Sizing, String> {
224    let points = zcu104_points();
225    let chips: Vec<Chip> = points.iter().map(zcu104_at).collect();
226    let summaries = points
227        .iter()
228        .zip(&chips)
229        .map(|(p, c)| PointSummary {
230            name: p.name,
231            pl_mhz: p.pl_hz.value / 1e6,
232            pl_provenance: p.pl_hz.provenance,
233            hp_ports: p.hp_ports,
234            ddr_read_gb_s: p.ddr_read.value / 1e9,
235            ddr_read_provenance: p.ddr_read.provenance,
236            sram_read_gb_s: c.tiers[0].bandwidth.value / 1e9,
237            int8_tops: c.matrix_rate(Precision::Int8).map_or(0.0, |f| f.value / 1e12),
238        })
239        .collect();
240    let deployment = Deployment::as_stored(ZCU104_CTX);
241    let mut models = Vec::new();
242    for &name in ZCU104_MODELS {
243        let path = decode_dir.join("headers").join(format!("{name}.json"));
244        let header = GgufHeader::read_ledger(&path).map_err(|e| format!("{}: {e}", path.display()))?;
245        let step = DecodeStep::of(&header, &deployment).map_err(|e| format!("{name}: {e:?}"))?;
246        let mut rows = Vec::new();
247        let mut refused = None;
248        for c in &chips {
249            match price(&step, &Machine::of(c.clone(), 1), Precision::Int8) {
250                Ok(cost) => {
251                    let on_chip = cost.residency.iter().find(|(k, _)| *k == TierKind::Sram).map_or(0.0, |(_, b)| *b);
252                    rows.push(SizingRow {
253                        point: c.name,
254                        floor_tok_s: cost.floor_tokens_per_s(),
255                        binds: cost.floor_binds,
256                        on_chip_share: on_chip / step.held_bytes(),
257                        provenance: cost.provenance,
258                    });
259                }
260                Err(DecodeRefusal::NotResident { needed, capacity }) => refused = Some((needed, capacity)),
261                Err(e) => return Err(format!("{name} on {}: {e:?}", c.name)),
262            }
263        }
264        let fit = match refused {
265            Some((needed, capacity)) if rows.is_empty() => Fit::NotResident { needed, capacity },
266            None => Fit::Resident { rows },
267            Some(_) => return Err(format!("{name}: resident at some points and not others, on one board's memory")),
268        };
269        models.push(ModelSizing {
270            model: name.to_string(),
271            file_bytes: header.file_bytes,
272            tied_head: !header.tensors.iter().any(|t| t.name == "output.weight"),
273            held_bytes: step.held_bytes(),
274            weight_bytes_per_token: step.weight_bytes(),
275            state_bytes_per_token: step.state_read_bytes() + step.state_write_bytes(),
276            fit,
277        });
278    }
279    let part = PartResources::xczu7ev();
280    Ok(Zcu104Sizing {
281        board: "ZCU104 (XCZU7EV-2FFVC1156)",
282        part: part.part,
283        ctx: ZCU104_CTX,
284        on_chip_bytes: part.block_bytes() as f64,
285        ddr_bytes: ddr_capacity().value,
286        points: summaries,
287        models,
288    })
289}