use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::path::PathBuf;
use taconite::cpu::par_rows;
use taconite::{Timing, bf16_to_f32, f32_to_bf16};
use taconite_bundle::Manifest;
#[cfg(feature = "direct")]
pub use taconite::direct::{Buffer, Kernel, Session};
#[cfg(all(feature = "xrt", not(feature = "direct")))]
pub use taconite::{Buffer, Kernel, Session};
#[cfg(not(any(feature = "xrt", feature = "direct")))]
compile_error!("taconite-reuse needs the `xrt` or the `direct` feature");
use crate::Error;
const BF16: usize = 2;
const PIECE: usize = 1 << 16;
const WAITS: usize = 50;
#[derive(Debug, Clone)]
pub struct GemmSpec {
pub k: usize,
pub n: usize,
pub lda: usize,
pub rows: usize,
pub a_elems: usize,
pub c_elems: usize,
}
#[derive(Debug, Clone)]
pub struct MambaSpec {
pub seqs: usize,
pub l: usize,
pub rows: usize,
pub in_elems: usize,
pub out_elems: usize,
}
pub struct Flat {
pub buf: Buffer,
pub elems: usize,
}
impl Flat {
pub fn bits_mut(&mut self, n: usize) -> &mut [u16] {
&mut self.buf.as_mut_slice::<u16>()[..n]
}
pub fn sync(&self, n: usize) -> Result<(), Error> {
self.buf.sub(0, n.min(self.elems) * BF16)?.sync_to_device()?;
Ok(())
}
pub fn fill_rows(&mut self, rows: usize, width: usize, sync_elems: usize, f: impl Fn(usize, &mut [u16]) + Sync) -> Result<(), Error> {
let n = rows * width;
if n > self.elems || sync_elems > self.elems {
return Err(Error::Input(format!("{n} elements into a buffer of {}", self.elems)));
}
let dst = &mut self.buf.as_mut_slice::<u16>()[..n];
par_rows(dst, width, |r0, piece| {
for (ri, row) in piece.chunks_mut(width).enumerate() {
f(r0 + ri, row);
}
});
self.sync(sync_elems.max(n))
}
pub fn zero(&mut self, n: usize) {
let dst = &mut self.buf.as_mut_slice::<u16>()[..n];
par_rows(dst, PIECE, |_, p| p.fill(0));
}
pub fn bits(&self, n: usize) -> Result<&[u16], Error> {
self.buf.sub(0, n * BF16)?.sync_from_device()?;
Ok(&self.buf.as_slice::<u16>()[..n])
}
pub fn get(&self, n: usize) -> Result<Vec<f32>, Error> {
let src = self.bits(n)?;
let mut out = vec![0f32; n];
par_rows(&mut out, PIECE, |r0, piece| {
let i0 = r0 * PIECE;
for (o, &b) in piece.iter_mut().zip(&src[i0..]) {
*o = bf16_to_f32(b);
}
});
Ok(out)
}
pub fn view(&self, offset: usize, elems: usize) -> Result<Buffer, Error> {
if offset + elems > self.elems {
return Err(Error::Bundle(format!("a view of [{offset}, {}) over a buffer of {}", offset + elems, self.elems)));
}
Ok(self.buf.sub(offset * BF16, elems * BF16)?)
}
}
pub fn to_bits(x: f32) -> u16 {
f32_to_bf16(x)
}
struct Src {
ctx: String,
xclbin: PathBuf,
name: String,
insts: PathBuf,
ops: u64,
}
pub struct Npu {
pub session: Session,
srcs: HashMap<String, Src>,
kernels: RefCell<HashMap<String, Kernel>>,
pub swaps: Cell<usize>,
used: RefCell<HashMap<String, u64>>,
tick: Cell<u64>,
pub gemms: HashMap<String, GemmSpec>,
pub mambas: HashMap<String, MambaSpec>,
pub chunk: usize,
pub max_chunks: usize,
pub contexts: usize,
}
impl Npu {
pub fn open(m: &Manifest) -> Result<Self, Error> {
let session = Session::open(0)?;
let mut srcs = HashMap::new();
let mut gemms = HashMap::new();
let mut mambas = HashMap::new();
let mut ctxs: Vec<PathBuf> = Vec::new();
for r in m.records() {
let key = format!("{}:{}", r.tag, r.field(0)?);
let ops = match r.tag.as_str() {
"gemm" => {
let g = GemmSpec {
k: r.get("K")?,
n: r.get("N")?,
lda: r.get("lda")?,
rows: r.get("rows")?,
a_elems: r.get("a_elems")?,
c_elems: r.get("c_elems")?,
};
let ops = (2 * g.rows * g.k * g.n) as u64;
gemms.insert(r.field(0)?.to_string(), g);
ops
}
"mamba" => {
let s = MambaSpec {
seqs: r.get("seqs")?,
l: r.get("L")?,
rows: r.get("rows")?,
in_elems: r.get("in_elems")?,
out_elems: r.get("out_elems")?,
};
mambas.insert(r.field(0)?.to_string(), s);
0
}
_ => continue,
};
let ctx = r.str("ctx")?.to_string();
let x = m.xclbin(&ctx)?;
if !ctxs.contains(&x.path) {
ctxs.push(x.path.clone());
}
let src = Src { ctx, xclbin: x.path.clone(), name: x.kernel.clone(), insts: m.path(r.str("insts")?), ops };
srcs.insert(key, src);
}
Ok(Npu {
session,
srcs,
kernels: Default::default(),
swaps: Cell::new(0),
used: Default::default(),
tick: Cell::new(0),
gemms,
mambas,
chunk: m.param_as("chunk")?,
max_chunks: m.param_as("max_chunks")?,
contexts: ctxs.len(),
})
}
pub fn upload(&self, bytes: &[u8]) -> Result<Buffer, Error> {
let mut b = self.session.alloc(bytes.len())?;
b.write(bytes)?;
Ok(b)
}
pub fn flat(&self, elems: usize) -> Result<Flat, Error> {
let mut buf = self.session.alloc(elems.max(1) * BF16)?;
buf.as_mut_slice::<u16>().fill(0);
buf.sync_to_device()?;
Ok(Flat { buf, elems })
}
pub fn ensure<'a>(&self, slot: &'a mut Option<Flat>, elems: usize) -> Result<&'a mut Flat, Error> {
if slot.as_ref().is_none_or(|f| f.elems < elems) {
*slot = Some(self.flat(elems + elems / 8)?);
}
Ok(slot.as_mut().unwrap())
}
fn with_kernel<T>(&self, key: &str, f: impl FnOnce(&Kernel) -> Result<T, Error>) -> Result<T, Error> {
let s = self.srcs.get(key).ok_or_else(|| Error::Bundle(format!("no kernel {key} in the bundle")))?;
self.tick.set(self.tick.get() + 1);
self.used.borrow_mut().insert(s.ctx.clone(), self.tick.get());
if !self.kernels.borrow().contains_key(key) {
let load = || {
self.session
.load_kernel(&s.xclbin, &s.insts, Some(&s.name), s.ops)
.map_err(|e| Error::Npu(format!("loading {key}: {e}")))
};
let mut waits = 0;
let k = loop {
match load() {
Ok(k) => break k,
Err(e) => {
let mut ks = self.kernels.borrow_mut();
let used = self.used.borrow();
let victim = ks
.keys()
.map(|k| self.srcs[k].ctx.as_str())
.filter(|c| *c != s.ctx)
.min_by_key(|c| used.get(*c).copied().unwrap_or(0))
.map(str::to_string);
let Some(victim) = victim else {
if waits >= WAITS {
return Err(Error::Npu(format!(
"{e}: every NPU hardware context is taken by other models or \
programs (NPU2 has 16); free some and try again"
)));
}
waits += 1;
drop((ks, used));
std::thread::sleep(std::time::Duration::from_millis(100));
continue;
};
ks.retain(|k, _| self.srcs[k].ctx != victim);
self.swaps.set(self.swaps.get() + 1);
}
}
};
self.kernels.borrow_mut().insert(key.to_string(), k);
}
let ks = self.kernels.borrow();
f(&ks[key])
}
pub fn release(&self) -> usize {
let mut ks = self.kernels.borrow_mut();
let mut ctxs: Vec<&str> = ks.keys().map(|k| self.srcs[k].ctx.as_str()).collect();
ctxs.sort();
ctxs.dedup();
let n = ctxs.len();
ks.clear();
n
}
fn run(&self, key: &str, args: &[&Buffer], t: &mut Timing, label: &str) -> Result<(), Error> {
let d = self.with_kernel(key, |k| k.run(args).map_err(|e| Error::Npu(format!("{key}: {e}"))))?;
t.add(label, d);
Ok(())
}
pub fn gemm(&self, g: &str, a: &Flat, a_row: usize, w: &Buffer, c: &Flat, c_row: usize, rows: usize, t: &mut Timing) -> Result<(), Error> {
let mut left = rows.div_ceil(self.chunk);
let mut r = 0;
while left > 0 {
let n = if left >= self.max_chunks { self.max_chunks } else { 1 << (usize::BITS - 1 - left.leading_zeros()) };
let key = format!("{g}.{n}");
let s = self.gemms.get(&key).ok_or_else(|| Error::Bundle(format!("no GEMM {key} in the bundle")))?;
let av = a.view((a_row + r) * s.lda, s.a_elems)?;
let cv = c.view((c_row + r) * s.n, s.c_elems)?;
self.run(&format!("gemm:{key}"), &[&av, w, &cv], t, &format!("npu:{g}"))?;
r += n * self.chunk;
left -= n;
}
Ok(())
}
pub fn gemm_spec(&self, g: &str) -> Result<&GemmSpec, Error> {
self.gemms.get(&format!("{g}.1")).ok_or_else(|| Error::Bundle(format!("no GEMM {g} in the bundle")))
}
pub fn mamba_spec(&self, phase: &str, seqs: usize, l: usize) -> Option<&MambaSpec> {
self.mambas.get(&format!("{phase}.{seqs}x{l}"))
}
pub fn mamba(&self, phase: &str, seqs: usize, l: usize, inp: &Flat, prm: &Buffer, out: &Flat, t: &mut Timing) -> Result<(), Error> {
let key = format!("{phase}.{seqs}x{l}");
let s = self.mambas.get(&key).ok_or_else(|| Error::Bundle(format!("no MambaScan {key} in the bundle")))?;
let (iv, ov) = (inp.view(0, s.in_elems)?, out.view(0, s.out_elems)?);
self.run(&format!("mamba:{key}"), &[&iv, prm, &ov], t, &format!("npu:{phase}"))
}
}