1use 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
30pub const HP_PORT_BYTES: f64 = 16.0;
32
33pub 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
42pub 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#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
54pub struct PlPoint {
55 pub name: &'static str,
57 pub pl_hz: Fact<f64>,
58 pub hp_ports: u32,
59 pub ddr_read: Fact<f64>,
61}
62
63pub 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
110pub fn zcu104() -> Chip {
112 zcu104_at(&zcu104_points()[0])
113}
114
115pub 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
157pub 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
162pub const ZCU104_CTX: u64 = 512;
164
165#[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#[derive(Clone, Debug, PartialEq, Serialize)]
180pub struct SizingRow {
181 pub point: &'static str,
182 pub floor_tok_s: f64,
184 pub binds: Bind,
185 pub on_chip_share: f64,
187 pub provenance: Provenance,
188}
189
190#[derive(Clone, Debug, PartialEq, Serialize)]
192pub enum Fit {
193 Resident { rows: Vec<SizingRow> },
194 NotResident { needed: f64, capacity: f64 },
195}
196
197#[derive(Clone, Debug, PartialEq, Serialize)]
199pub struct ModelSizing {
200 pub model: String,
201 pub file_bytes: u64,
202 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#[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
222pub 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}