use std::collections::HashMap;
use std::time::Duration;
#[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!("no NPU path: enable feature `xrt` (the default) or `direct`");
use crate::Error;
use crate::bundle::{GemmSpec, Manifest, MhaSpec};
use crate::cpu::par_rows;
pub fn round_up(x: usize, m: usize) -> usize {
x.div_ceil(m) * m
}
const BF16: usize = 2;
struct Source {
ctx: String,
xclbin: std::path::PathBuf,
insts: std::path::PathBuf,
name: String,
ops: u64,
}
pub struct Npu {
pub session: Session,
sources: HashMap<String, Source>,
loaded: HashMap<String, Kernel>,
lru: Vec<String>,
pub loads: usize,
pub evictions: usize,
cap: usize,
pub gemms: HashMap<String, GemmSpec>,
pub mhas: HashMap<String, MhaSpec>,
}
pub struct Io {
pub key: String,
pub a: Buffer,
pub c: Buffer,
chunks: Vec<(Buffer, Buffer)>,
chained: bool,
pub rows: usize,
pub n: usize,
}
pub fn push<T: Copy + Send + Sync>(src: &[T], b: &mut Buffer) -> Result<(), Error> {
let dst = &mut b.as_mut_slice::<T>()[..src.len()];
par_rows(dst, COPY, |r0, piece| piece.copy_from_slice(&src[r0 * COPY..][..piece.len()]));
Ok(b.sync_to_device()?)
}
pub fn pull<T: Copy + Default + Send + Sync>(b: &Buffer, n: usize) -> Result<Vec<T>, Error> {
b.sync_from_device()?;
let src = &b.as_slice::<T>()[..n];
let mut dst = vec![T::default(); n];
par_rows(&mut dst, COPY, |r0, piece| piece.copy_from_slice(&src[r0 * COPY..][..piece.len()]));
Ok(dst)
}
const COPY: usize = 1 << 16;
impl Io {
pub fn set_a(&mut self, src: &[u16]) -> Result<(), Error> {
assert!(!self.chained, "{}: A is another kernel's output", self.key);
push(src, &mut self.a)
}
pub fn sync_a(&self) -> Result<(), Error> {
Ok(self.a.sync_to_device()?)
}
pub fn get_c(&self, rows: usize) -> Result<Vec<u16>, Error> {
pull(&self.c, rows * self.n)
}
}
pub struct MhaIo {
pub key: String,
pub q: Buffer,
pub k: Buffer,
pub v: Buffer,
pub o: Buffer,
}
impl Npu {
pub fn open(m: &Manifest) -> Result<Self, Error> {
let session = Session::open(0)?;
let mut sources = HashMap::new();
let mut order = vec![];
for g in m.gemms.values() {
let x = m.xclbin(&g.ctx)?;
let ops = (2 * g.m * g.k * g.n) as u64;
let s = Source {
ctx: g.ctx.clone(),
xclbin: x.path.clone(),
insts: g.insts.clone(),
name: x.kernel.clone(),
ops,
};
sources.insert(g.key.clone(), s);
order.push(g.key.clone());
}
for o in m.ops.values() {
let s = Source {
ctx: o.key.clone(),
xclbin: o.xclbin.clone(),
insts: o.insts.clone(),
name: o.name.clone(),
ops: 0,
};
sources.insert(o.key.clone(), s);
order.push(o.key.clone());
}
for h in m.mhas.values() {
let ops = (4 * h.heads * h.seq * h.seq * h.d) as u64;
let s = Source {
ctx: h.key.clone(),
xclbin: h.xclbin.clone(),
insts: h.insts.clone(),
name: h.name.clone(),
ops,
};
sources.insert(h.key.clone(), s);
order.push(h.key.clone());
}
let mut npu = Npu {
session,
sources,
loaded: HashMap::new(),
lru: vec![],
loads: 0,
evictions: 0,
cap: std::env::var("SAM3_MAX_CONTEXTS").ok().and_then(|v| v.parse().ok()).unwrap_or(usize::MAX),
gemms: m.gemms.clone(),
mhas: m.mhas.clone(),
};
order.sort();
for key in order {
let ctx = &npu.sources[&key].ctx;
if !npu.lru.contains(ctx) && npu.lru.len() >= npu.cap {
break;
}
match npu.try_load(&key) {
Ok(()) => {}
Err(e) if is_full(&e) => break,
Err(e) => return Err(Error::Npu(format!("loading {key}: {e}"))),
}
}
Ok(npu)
}
fn try_load(&mut self, key: &str) -> Result<(), taconite::Error> {
let s = &self.sources[key];
let k = self.session.load_kernel(&s.xclbin, &s.insts, Some(&s.name), s.ops)?;
let ctx = s.ctx.clone();
self.loaded.insert(key.to_string(), k);
if !self.lru.contains(&ctx) {
self.lru.push(ctx);
self.loads += 1;
}
Ok(())
}
fn kernel(&mut self, key: &str) -> Result<&Kernel, Error> {
let ctx = self.sources.get(key).ok_or_else(|| Error::Bundle(format!("no kernel {key}")))?.ctx.clone();
if !self.loaded.contains_key(key) {
while !self.lru.contains(&ctx) && self.lru.len() >= self.cap {
self.evict_lru(&ctx);
}
loop {
match self.try_load(key) {
Ok(()) => break,
Err(e) if is_full(&e) => {
if !self.evict_lru(&ctx) {
return Err(Error::Npu(format!("{key}: no hardware context available: {e}")));
}
}
Err(e) => return Err(Error::Npu(format!("loading {key}: {e}"))),
}
}
}
if let Some(i) = self.lru.iter().position(|c| *c == ctx) {
let c = self.lru.remove(i);
self.lru.push(c);
}
Ok(&self.loaded[key])
}
pub fn spec(&self, key: &str) -> Result<&GemmSpec, Error> {
self.gemms.get(key).ok_or_else(|| Error::Bundle(format!("no kernel {key}")))
}
pub fn upload(&self, bytes: &[u8]) -> Result<Buffer, Error> {
let mut b = self.session.alloc(bytes.len())?;
b.write(bytes)?;
Ok(b)
}
pub fn weight_slot(&self, key: &str) -> Result<Buffer, Error> {
Ok(self.session.alloc(self.spec(key)?.b_bytes)?)
}
pub fn io(&self, key: &str, rows: usize) -> Result<Io, Error> {
let g = self.spec(key)?;
assert_eq!(g.lda, g.k, "{key}: overlapping A, use conv_io");
let rows = round_up(rows, g.m);
let a = self.session.alloc(rows * g.k * BF16)?;
let c = self.session.alloc(rows * g.n * BF16)?;
let mut chunks = Vec::new();
for i in 0..rows / g.m {
chunks
.push((a.sub(i * g.m * g.k * BF16, g.m * g.k * BF16)?, c.sub(i * g.m * g.n * BF16, g.m * g.n * BF16)?));
}
Ok(Io { key: key.into(), a, c, chunks, chained: false, rows, n: g.n })
}
pub fn io_chained(&self, key: &str, src: &Io) -> Result<Io, Error> {
let g = self.spec(key)?;
assert_eq!(src.n, g.k, "{key}: A width {} != K {}", src.n, g.k);
let rows = src.rows;
let a = src.c.sub(0, rows * g.k * BF16)?;
let c = self.session.alloc(rows * g.n * BF16)?;
let mut chunks = Vec::new();
for i in 0..rows / g.m {
chunks.push((
src.c.sub(i * g.m * g.k * BF16, g.m * g.k * BF16)?,
c.sub(i * g.m * g.n * BF16, g.m * g.n * BF16)?,
));
}
Ok(Io { key: key.into(), a, c, chunks, chained: true, rows, n: g.n })
}
pub fn conv_io(&self, key: &str, h: usize, w: usize) -> Result<Io, Error> {
let g = self.spec(key)?;
let d = (g.k - g.lda) / 2; let p = g.lda / d;
let rows = round_up((h * (w + 2)).div_ceil(p), g.m);
let a = self.session.alloc((rows * p + 2) * d * BF16)?;
let c = self.session.alloc(rows * g.n * BF16)?;
let ext = (g.m - 1) * g.lda + g.k;
let mut chunks = Vec::new();
for i in 0..rows / g.m {
chunks.push((a.sub(i * g.m * g.lda * BF16, ext * BF16)?, c.sub(i * g.m * g.n * BF16, g.m * g.n * BF16)?));
}
Ok(Io { key: key.into(), a, c, chunks, chained: false, rows, n: g.n })
}
pub fn mha_io(&self, key: &str) -> Result<MhaIo, Error> {
let h = self.mhas.get(key).ok_or_else(|| Error::Bundle(format!("no MHA {key}")))?;
let buf = || self.session.alloc(h.elems * BF16);
Ok(MhaIo { key: key.into(), q: buf()?, k: buf()?, v: buf()?, o: buf()? })
}
fn evict_lru(&mut self, keep: &str) -> bool {
let Some(victim) = self.lru.iter().find(|c| *c != keep).cloned() else {
return false;
};
self.loaded.retain(|k, _| self.sources[k].ctx != victim);
self.lru.retain(|c| *c != victim);
self.evictions += 1;
true
}
pub fn run(&mut self, io: &Io, w: &Buffer) -> Result<Duration, Error> {
let k = self.kernel(&io.key)?;
let mut t = Duration::ZERO;
for (a, c) in &io.chunks {
t += k.run(&[a, w, c]).map_err(|e| Error::Npu(format!("{}: {e}", io.key)))?;
}
Ok(t)
}
pub fn run_synced(&mut self, io: &Io, w: &Buffer) -> Result<Duration, Error> {
if !io.chained {
io.sync_a()?;
}
self.run(io, w)
}
pub fn run_args(&mut self, key: &str, args: &[&Buffer]) -> Result<Duration, Error> {
self.kernel(key)?.run(args).map_err(|e| Error::Npu(format!("{key}: {e}")))
}
pub fn run_mha(&mut self, m: &MhaIo) -> Result<Duration, Error> {
self.kernel(&m.key)?.run(&[&m.q, &m.k, &m.v, &m.o]).map_err(|e| Error::Npu(format!("{}: {e}", m.key)))
}
}
fn is_full(e: &taconite::Error) -> bool {
e.to_string().contains("CREATE_HWCTX")
}