use crate::model::{Layer, Model};
use crate::tune::{RequantPrecision, Tune};
pub mod arena;
pub mod fold_zero_zp;
pub mod fuse_conv_relu;
pub mod parallelism;
pub mod shared_requant;
pub mod ternary_fast_path;
#[derive(Debug, Clone)]
pub struct Hints {
pub fast_mac: bool,
pub ternary_fast_path: bool,
pub shared_requant: Option<(i32, i32)>,
pub bram_clock_enable: bool,
pub requant_precision: RequantPrecision,
pub parallelism: u32,
pub ic_parallelism: u32,
pub fuses_relu: bool,
pub elided: bool,
pub bram_slot_in: Option<u8>,
pub bram_slot_out: Option<u8>,
}
impl Default for Hints {
fn default() -> Self {
Self {
fast_mac: false,
ternary_fast_path: false,
shared_requant: None,
bram_clock_enable: false,
requant_precision: RequantPrecision::default(),
parallelism: 1,
ic_parallelism: 1,
fuses_relu: false,
elided: false,
bram_slot_in: None,
bram_slot_out: None,
}
}
}
pub struct OptimizedModel {
pub model: Model,
pub hints: Vec<Hints>,
pub tune: Tune,
pub arena_bank: std::collections::BTreeMap<u8, u8>,
}
impl OptimizedModel {
pub fn hints_for(&self, layer_idx: usize) -> &Hints {
&self.hints[layer_idx]
}
}
pub fn optimize(model: &Model, tune: &Tune) -> OptimizedModel {
let ir = crate::ir::to_graph(model);
let errors = rlx_ir::verify::verify(&ir.graph);
if !errors.is_empty() {
let msg = errors
.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join("\n ");
panic!("rlx-fpga: IR verifier rejected the model graph:\n {msg}");
}
let mut hints: Vec<Hints> = Vec::with_capacity(model.layers.len());
for layer in &model.layers {
let mut h = Hints {
requant_precision: tune.requant_precision,
parallelism: parallelism::layer_parallelism(layer, tune.parallelism),
ic_parallelism: 1, ..Hints::default()
};
if tune.fold_zero_zp && fold_zero_zp::layer_has_zero_zps(layer) {
h.fast_mac = true;
}
if tune.ternary_fast_path && ternary_fast_path::is_ternary(layer) {
h.ternary_fast_path = true;
h.ic_parallelism = parallelism::layer_ic_parallelism(layer, tune.ic_parallelism);
}
if tune.shared_requant
&& let Some(pair) = shared_requant::uniform_requant(layer)
{
h.shared_requant = Some(pair);
}
if tune.bram_clock_enable {
h.bram_clock_enable = true;
}
hints.push(h);
}
if tune.fuse_conv_relu {
fuse_conv_relu::run(model, &mut hints);
}
let arena_bank = if tune.arena_plan {
arena::run(model, &mut hints)
} else {
std::collections::BTreeMap::new()
};
OptimizedModel {
model: model.clone(),
hints,
tune: *tune,
arena_bank,
}
}
pub fn optimize_default(model: &Model) -> OptimizedModel {
optimize(model, &Tune::default())
}
pub fn summary(opt: &OptimizedModel) -> String {
let n = opt.hints.len();
let fast_mac = opt.hints.iter().filter(|h| h.fast_mac).count();
let ternary = opt.hints.iter().filter(|h| h.ternary_fast_path).count();
let shared_r = opt
.hints
.iter()
.filter(|h| h.shared_requant.is_some())
.count();
let bram_en = opt.hints.iter().filter(|h| h.bram_clock_enable).count();
let par_eligible = opt.hints.iter().filter(|h| h.parallelism > 1).count();
let max_p = opt.hints.iter().map(|h| h.parallelism).max().unwrap_or(1);
let fused = opt.hints.iter().filter(|h| h.fuses_relu).count();
let elided = opt.hints.iter().filter(|h| h.elided).count();
let arena_slots: std::collections::BTreeSet<u8> = opt
.hints
.iter()
.flat_map(|h| [h.bram_slot_in, h.bram_slot_out])
.flatten()
.collect();
format!(
"passes: fast_mac={fast_mac}/{n} ternary={ternary}/{n} \
shared_requant={shared_r}/{n} bram_en={bram_en}/{n} \
P_layers={par_eligible}/{n} (max P={max_p}) \
fuse_conv_relu={fused} (elided={elided}) \
arena={} slots ({})",
arena_slots.len(),
opt.tune
)
}
pub(crate) fn conv_dense_zps(layer: &Layer) -> Option<(i32, i32)> {
match layer {
Layer::Conv2d { x_zp, w_zp, .. } | Layer::Dense { x_zp, w_zp, .. } => Some((*x_zp, *w_zp)),
_ => None,
}
}
pub(crate) fn conv_dense_requant(layer: &Layer) -> Option<&[(i32, i32)]> {
match layer {
Layer::Conv2d { requant, .. } | Layer::Dense { requant, .. } => Some(requant),
_ => None,
}
}