1use std::collections::{HashMap, VecDeque};
61use std::fmt;
62use std::path::{Path, PathBuf};
63use std::time::{Duration, Instant};
64
65use taconite_bundle::{DType, Manifest, Record, Store};
66#[cfg(feature = "direct")]
69use taconite::direct::{Buffer, Kernel, Session};
70#[cfg(all(feature = "xrt", not(feature = "direct")))]
71use taconite::{Buffer, Kernel, Session};
72#[cfg(not(any(feature = "xrt", feature = "direct")))]
73compile_error!("no NPU path: enable feature `xrt` (the default) or `direct`");
74
75pub const BACKEND: &str = if cfg!(feature = "direct") {
77 "direct"
78} else {
79 "xrt"
80};
81
82pub const BUNDLE_VERSION: u32 = 1;
84
85#[derive(Debug)]
92pub enum Error {
93 Io(PathBuf, std::io::Error),
95 Plan(String),
98 Xrt(String),
101 Input(String),
103}
104
105impl fmt::Display for Error {
106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107 match self {
108 Error::Io(p, e) => write!(f, "cannot read {}: {e}", p.display()),
109 Error::Plan(s) => write!(f, "bad bundle: {s}"),
110 Error::Xrt(s) => write!(f, "NPU ({BACKEND}): {s}"),
111 Error::Input(s) => write!(f, "bad input: {s}"),
112 }
113 }
114}
115
116impl std::error::Error for Error {}
117
118impl From<taconite_bundle::Error> for Error {
119 fn from(e: taconite_bundle::Error) -> Self {
120 match e {
121 taconite_bundle::Error::Io(p, e) => Error::Io(p, e),
122 taconite_bundle::Error::Format(m) => Error::Plan(m),
123 }
124 }
125}
126
127struct Npu {
137 session: Session,
138 cache: VecDeque<Resident>,
139 cap: usize,
140 timing: Option<NpuTiming>,
141}
142
143struct Resident {
144 key: String,
145 kernel: Kernel,
146 args: Option<[Buffer; 3]>,
148}
149
150#[derive(Default)]
152struct NpuTiming {
153 load: Duration,
154 run: Duration,
155 wait: Duration,
156 loads: usize,
157 hits: usize,
158 evictions: usize,
159 runs: usize,
160}
161
162const DEFAULT_CTX_CACHE: usize = 16;
164
165impl Npu {
166 fn open() -> Result<Npu, Error> {
167 let session = Session::open(0).map_err(npu_err("open"))?;
168 let cap = std::env::var("TACONITE_CTX_CACHE")
169 .ok()
170 .and_then(|v| v.parse().ok())
171 .filter(|&v: &usize| v >= 1)
172 .unwrap_or(DEFAULT_CTX_CACHE);
173 let timing = std::env::var_os("TACONITE_TIMING").map(|_| NpuTiming::default());
174 Ok(Npu {
175 session,
176 cache: VecDeque::new(),
177 cap,
178 timing,
179 })
180 }
181
182 fn ensure(
187 &mut self,
188 key: &str,
189 xclbin: &Path,
190 insts: &Path,
191 kernel: &str,
192 ) -> Result<(), Error> {
193 if let Some(i) = self.cache.iter().position(|r| r.key == key) {
194 let r = self.cache.remove(i).expect("position is in range");
195 self.cache.push_front(r);
196 if let Some(t) = &mut self.timing {
197 t.hits += 1;
198 }
199 return Ok(());
200 }
201 let t0 = Instant::now();
202 let mut evicted = 0;
203 while self.cache.len() >= self.cap {
204 self.cache.pop_back();
205 evicted += 1;
206 }
207 let kernel = loop {
208 match self.session.load_kernel(xclbin, insts, Some(kernel), 0) {
209 Ok(k) => break k,
210 Err(e) => {
211 if self.cache.pop_back().is_none() {
212 return Err(npu_err(&format!("load({key})"))(e));
213 }
214 evicted += 1;
215 }
216 }
217 };
218 self.cache.push_front(Resident {
219 key: key.to_string(),
220 kernel,
221 args: None,
222 });
223 if let Some(t) = &mut self.timing {
224 t.load += t0.elapsed();
225 t.loads += 1;
226 t.evictions += evicted;
227 }
228 Ok(())
229 }
230
231 fn run(
234 &mut self,
235 key: &str,
236 input: &[u16],
237 weights: &[u16],
238 out_len: usize,
239 ) -> Result<Vec<u16>, Error> {
240 let t0 = Instant::now();
241 let err = npu_err(key);
242 let r = self
243 .cache
244 .iter_mut()
245 .find(|r| r.key == key)
246 .ok_or_else(|| Error::Xrt(format!("{key}: kernel not loaded")))?;
247 let sizes = [input.len(), weights.len(), out_len];
248 if r.args
249 .as_ref()
250 .map(|a| a.each_ref().map(|b| b.len_bytes() / 2))
251 != Some(sizes)
252 {
253 let s = &self.session;
254 r.args = Some([
255 s.alloc_of::<u16>(sizes[0]).map_err(&err)?,
256 s.alloc_of::<u16>(sizes[1]).map_err(&err)?,
257 s.alloc_of::<u16>(sizes[2]).map_err(&err)?,
258 ]);
259 }
260 let [a, w, c] = r.args.as_mut().expect("allocated above");
261 a.write(input).map_err(&err)?;
262 w.write(weights).map_err(&err)?;
263 let wait = r.kernel.run(&[a, w, c]).map_err(&err)?;
264 c.sync_from_device().map_err(&err)?;
265 let out = c.as_slice::<u16>().to_vec();
266 if let Some(t) = &mut self.timing {
267 t.run += t0.elapsed();
268 t.wait += wait;
269 t.runs += 1;
270 }
271 Ok(out)
272 }
273}
274
275impl Drop for Npu {
276 fn drop(&mut self) {
277 if let Some(t) = &self.timing {
278 eprintln!(
279 "[{BACKEND}] loads={} hits={} evict={} runs={} load={:.2}s run={:.2}s (wait={:.2}s)",
280 t.loads,
281 t.hits,
282 t.evictions,
283 t.runs,
284 t.load.as_secs_f64(),
285 t.run.as_secs_f64(),
286 t.wait.as_secs_f64()
287 );
288 }
289 }
290}
291
292fn npu_err(what: &str) -> impl Fn(taconite::Error) -> Error + '_ {
293 move |e| Error::Xrt(format!("{what}: {e}"))
294}
295
296#[inline]
301fn bf16_to_f32(x: u16) -> f32 {
302 f32::from_bits((x as u32) << 16)
303}
304
305#[inline]
306fn f32_to_bf16(f: f32) -> u16 {
307 let bits = f.to_bits();
308 if (bits & 0x7fff_ffff) > 0x7f80_0000 {
309 return 0x7fc0; }
311 let rounding_bias = 0x7fff + ((bits >> 16) & 1); ((bits + rounding_bias) >> 16) as u16
313}
314
315#[derive(Clone)]
320struct Buf {
321 v: Vec<u16>,
322 c: usize,
323 h: usize,
324 w: usize,
325}
326
327#[inline]
328fn tidx(c: usize, w: usize, ch: usize, y: usize, x: usize) -> usize {
329 (((y * (c / 8)) + ch / 8) * w + x) * 8 + (ch % 8)
330}
331
332fn slice_groups_wpad(src: &Buf, g0: usize, ngroups: usize, wp: usize) -> Vec<u16> {
336 let icg = ngroups * 8;
337 let mut out = vec![0u16; src.h * icg * wp];
338 for y in 0..src.h {
339 for gg in 0..ngroups {
340 for x in 0..src.w {
341 for lane in 0..8 {
342 let s = (((y * (src.c / 8)) + (g0 + gg)) * src.w + x) * 8 + lane;
343 let d = (((y * ngroups) + gg) * wp + x) * 8 + lane;
344 out[d] = src.v[s];
345 }
346 }
347 }
348 }
349 out
350}
351
352fn affine_tiled(src: &Buf, scale: &[u16], shift: &[u16]) -> Buf {
354 let mut v = vec![0u16; src.v.len()];
355 for y in 0..src.h {
356 for ch in 0..src.c {
357 for x in 0..src.w {
358 let i = tidx(src.c, src.w, ch, y, x);
359 let r = bf16_to_f32(src.v[i]) * bf16_to_f32(scale[ch]) + bf16_to_f32(shift[ch]);
360 v[i] = f32_to_bf16(r);
361 }
362 }
363 }
364 Buf {
365 v,
366 c: src.c,
367 h: src.h,
368 w: src.w,
369 }
370}
371
372fn prelu_bias_tiled(buf: &mut Buf, alpha: &[u16], bias: &[u16]) {
375 for y in 0..buf.h {
376 for ch in 0..buf.c {
377 for x in 0..buf.w {
378 let i = tidx(buf.c, buf.w, ch, y, x);
379 let t = bf16_to_f32(f32_to_bf16(bf16_to_f32(buf.v[i]) + bf16_to_f32(bias[ch])));
380 let r = if t < 0.0 {
381 bf16_to_f32(alpha[ch]) * t
382 } else {
383 t
384 };
385 buf.v[i] = f32_to_bf16(r);
386 }
387 }
388 }
389}
390
391fn bias_tiled(buf: &mut Buf, bias: &[u16]) {
393 for y in 0..buf.h {
394 for ch in 0..buf.c {
395 for x in 0..buf.w {
396 let i = tidx(buf.c, buf.w, ch, y, x);
397 buf.v[i] = f32_to_bf16(bf16_to_f32(buf.v[i]) + bf16_to_f32(bias[ch]));
398 }
399 }
400 }
401}
402
403fn add_tiled(a: &Buf, b: &Buf) -> Buf {
404 let v =
405 a.v.iter()
406 .zip(&b.v)
407 .map(|(&x, &y)| f32_to_bf16(bf16_to_f32(x) + bf16_to_f32(y)))
408 .collect();
409 Buf {
410 v,
411 c: a.c,
412 h: a.h,
413 w: a.w,
414 }
415}
416
417fn subsample_tiled(src: &Buf, stride: usize) -> Buf {
418 let (oh, ow) = (src.h / stride, src.w / stride);
419 let mut out = vec![0u16; oh * (src.c / 8) * ow * 8];
420 for y in 0..oh {
421 for g in 0..(src.c / 8) {
422 for x in 0..ow {
423 for lane in 0..8 {
424 let s = (((stride * y) * (src.c / 8) + g) * src.w + stride * x) * 8 + lane;
425 let d = ((y * (src.c / 8) + g) * ow + x) * 8 + lane;
426 out[d] = src.v[s];
427 }
428 }
429 }
430 }
431 Buf {
432 v: out,
433 c: src.c,
434 h: oh,
435 w: ow,
436 }
437}
438
439fn tiled_to_nchw_f32(b: &Buf) -> Vec<f32> {
441 let mut out = vec![0f32; b.c * b.h * b.w];
442 for ch in 0..b.c {
443 for y in 0..b.h {
444 for x in 0..b.w {
445 out[(ch * b.h + y) * b.w + x] = bf16_to_f32(b.v[tidx(b.c, b.w, ch, y, x)]);
446 }
447 }
448 }
449 out
450}
451
452pub fn cosine(a: &[f32], b: &[f32]) -> f64 {
454 let (mut dot, mut na, mut nb) = (0f64, 0f64, 0f64);
455 for (&x, &y) in a.iter().zip(b) {
456 dot += x as f64 * y as f64;
457 na += x as f64 * x as f64;
458 nb += y as f64 * y as f64;
459 }
460 if na == 0.0 || nb == 0.0 {
461 0.0
462 } else {
463 dot / (na.sqrt() * nb.sqrt())
464 }
465}
466
467struct Conv {
475 input: u32,
476 out: u32,
477 c0: usize,
478 icg: usize,
479 wp: usize,
480 oc: usize,
481 oh: usize,
482 ow: usize,
483 owp: usize,
484 ctx: String,
485 xclbin: PathBuf,
486 kernel: String,
487 insts: PathBuf,
488 wt: String,
489 first: bool,
490 last: bool,
491}
492
493enum Step {
496 Input {
497 buf: u32,
498 c: usize,
499 h: usize,
500 w: usize,
501 tensor: String,
502 },
503 Conv(Conv),
504 Affine {
505 input: u32,
506 out: u32,
507 scale: String,
508 shift: String,
509 },
510 PreluBias {
511 buf: u32,
512 alpha: String,
513 bias: String,
514 },
515 Bias {
516 buf: u32,
517 bias: String,
518 },
519 Subsample {
520 input: u32,
521 out: u32,
522 stride: usize,
523 },
524 Add {
525 a: u32,
526 b: u32,
527 out: u32,
528 },
529 Head {
530 input: u32,
531 k: usize,
532 n: usize,
533 bt: String,
534 b2: String,
535 },
536}
537
538type Shape = (usize, usize, usize);
540
541fn read(shapes: &HashMap<u32, Shape>, r: &Record, key: &str) -> Result<(u32, Shape), Error> {
543 let id: u32 = r.get(key)?;
544 match shapes.get(&id) {
545 Some(&s) => Ok((id, s)),
546 None => Err(r
547 .error(format!("{key}={id} is not a defined buffer here"))
548 .into()),
549 }
550}
551
552fn dims(r: &Record) -> Result<Shape, Error> {
553 Ok((r.get("c")?, r.get("h")?, r.get("w")?))
554}
555
556fn same(r: &Record, what: &str, got: Shape, want: Shape) -> Result<(), Error> {
557 if got != want {
558 return Err(r
559 .error(format!(
560 "{what} is {got:?} (c, h, w), the step says {want:?}"
561 ))
562 .into());
563 }
564 Ok(())
565}
566
567fn tensor(
569 store: &Store,
570 r: &Record,
571 key: &str,
572 dtype: DType,
573 elems: usize,
574) -> Result<String, Error> {
575 let name = r.str(key)?;
576 store.expect(name, dtype, elems).map_err(|e| r.error(e))?;
577 Ok(name.to_string())
578}
579
580fn parse_steps(m: &Manifest, store: &Store) -> Result<Vec<Step>, Error> {
584 let mut shapes: HashMap<u32, Shape> = HashMap::new();
585 let mut steps = Vec::new();
586 for r in m.records() {
587 let step = match r.tag.as_str() {
588 "input" => {
589 let (c, h, w) = dims(r)?;
590 if c == 0 || c % 8 != 0 || h == 0 || w == 0 {
591 return Err(r
592 .error("c must be a positive multiple of 8, h and w positive")
593 .into());
594 }
595 let tensor = tensor(store, r, "tensor", DType::Bf16, c * h * w)?;
596 let buf = r.get("buf")?;
597 shapes.insert(buf, (c, h, w));
598 Step::Input {
599 buf,
600 c,
601 h,
602 w,
603 tensor,
604 }
605 }
606 "conv" => {
607 let (input, s) = read(&shapes, r, "in")?;
608 let (ict, h, w): Shape = (r.get("ict")?, r.get("h")?, r.get("w")?);
609 same(r, "the input", s, (ict, h, w))?;
610 let (c0, icg, wp): (usize, usize, usize) =
611 (r.get("c0")?, r.get("icg")?, r.get("wp")?);
612 let (oc, oh, ow, owp): (usize, usize, usize, usize) =
613 (r.get("oc")?, r.get("oh")?, r.get("ow")?, r.get("owp")?);
614 if icg == 0
615 || icg % 8 != 0
616 || 8 * c0 + icg > ict
617 || oc % 8 != 0
618 || wp < w
619 || owp < ow
620 {
621 return Err(r
622 .error("inconsistent channel group or padded widths")
623 .into());
624 }
625 let x = m.xclbin(r.str("ctx")?).map_err(|e| r.error(e))?;
626 let wt = r.str("wt")?;
627 let e = store.entry(wt).map_err(|e| r.error(e))?;
628 if e.dtype != DType::Bf16 {
629 return Err(r
630 .error(format!("tensor {wt} is {:?}, wanted Bf16", e.dtype))
631 .into());
632 }
633 let (out, last) = (r.get("out")?, r.flag("last")?);
634 if last {
635 shapes.insert(out, (oc, oh, ow));
636 }
637 Step::Conv(Conv {
638 input,
639 out,
640 c0,
641 icg,
642 wp,
643 oc,
644 oh,
645 ow,
646 owp,
647 ctx: x.key.clone(),
648 xclbin: x.path.clone(),
649 kernel: x.kernel.clone(),
650 insts: m.path(r.str("insts")?),
651 wt: wt.to_string(),
652 first: r.flag("first")?,
653 last,
654 })
655 }
656 "affine" => {
657 let (input, s) = read(&shapes, r, "in")?;
658 let d = dims(r)?;
659 same(r, "the input", s, d)?;
660 let scale = tensor(store, r, "scale", DType::Bf16, d.0)?;
661 let shift = tensor(store, r, "shift", DType::Bf16, d.0)?;
662 let out = r.get("out")?;
663 shapes.insert(out, d);
664 Step::Affine {
665 input,
666 out,
667 scale,
668 shift,
669 }
670 }
671 "prelu_bias" => {
672 let (buf, s) = read(&shapes, r, "buf")?;
673 same(r, "the buffer", s, dims(r)?)?;
674 let alpha = tensor(store, r, "alpha", DType::Bf16, s.0)?;
675 let bias = tensor(store, r, "bias", DType::Bf16, s.0)?;
676 Step::PreluBias { buf, alpha, bias }
677 }
678 "bias" => {
679 let (buf, s) = read(&shapes, r, "buf")?;
680 same(r, "the buffer", s, dims(r)?)?;
681 let bias = tensor(store, r, "bias", DType::Bf16, s.0)?;
682 Step::Bias { buf, bias }
683 }
684 "subsample" => {
685 let (input, s) = read(&shapes, r, "in")?;
686 let (c, h, w) = dims(r)?;
687 same(r, "the input", s, (c, h, w))?;
688 let stride: usize = r.get("stride")?;
689 if stride == 0 {
690 return Err(r.error("stride must be positive").into());
691 }
692 let out = r.get("out")?;
693 shapes.insert(out, (c, h / stride, w / stride));
694 Step::Subsample { input, out, stride }
695 }
696 "add" => {
697 let (a, sa) = read(&shapes, r, "a")?;
698 let (b, sb) = read(&shapes, r, "b")?;
699 let d = dims(r)?;
700 same(r, "a", sa, d)?;
701 same(r, "b", sb, d)?;
702 let out = r.get("out")?;
703 shapes.insert(out, d);
704 Step::Add { a, b, out }
705 }
706 "head" => {
707 let (input, s) = read(&shapes, r, "in")?;
708 let d = dims(r)?;
709 same(r, "the input", s, d)?;
710 let (k, n): (usize, usize) = (r.get("k")?, r.get("n")?);
711 if d.0 * d.1 * d.2 != k {
712 return Err(r
713 .error(format!("k={k} is not the input's c * h * w"))
714 .into());
715 }
716 let bt = tensor(store, r, "bT", DType::Bf16, k * n)?;
717 let b2 = tensor(store, r, "b2", DType::F32, n)?;
718 Step::Head {
719 input,
720 k,
721 n,
722 bt,
723 b2,
724 }
725 }
726 other => return Err(r.error(format!("unknown step {other}")).into()),
727 };
728 steps.push(step);
729 }
730 Ok(steps)
731}
732
733pub struct IrEmbedder {
743 npu: Npu,
744 store: Store,
745 steps: Vec<Step>,
746 head_bt: HashMap<String, Vec<f32>>,
748 model: Option<String>,
749 in_c: usize, in_h: usize,
751 in_w: usize,
752 embed_dim: usize,
753}
754
755const _: () = {
759 const fn send<T: Send>() {}
760 send::<IrEmbedder>()
761};
762
763impl IrEmbedder {
764 pub fn load(bundle_dir: impl AsRef<Path>) -> Result<Self, Error> {
767 let dir = bundle_dir.as_ref();
768 if !dir.join(Manifest::FILE).exists() && dir.join("plan.txt").exists() {
769 return Err(Error::Plan(format!(
770 "{} is an old plan.txt bundle; re-export it with export_ir18.py / export_ir101.py",
771 dir.display()
772 )));
773 }
774 let m = Manifest::load(dir, BUNDLE_VERSION)?;
775 let store = Store::load(dir)?;
776 let steps = parse_steps(&m, &store)?;
777 let inputs: Vec<Shape> = steps
778 .iter()
779 .filter_map(|s| match s {
780 Step::Input { c, h, w, .. } => Some((*c, *h, *w)),
781 _ => None,
782 })
783 .collect();
784 let [(in_c, in_h, in_w)] = inputs[..] else {
785 return Err(Error::Plan(
786 "the bundle needs exactly one input step".into(),
787 ));
788 };
789 let mut head_bt = HashMap::new();
790 let mut embed_dim = None;
791 for s in &steps {
792 if let Step::Head { bt, n, .. } = s {
793 embed_dim = Some(*n);
794 if !head_bt.contains_key(bt) {
795 let v: Vec<f32> = store.bf16(bt)?.iter().map(|&x| bf16_to_f32(x)).collect();
796 head_bt.insert(bt.clone(), v);
797 }
798 }
799 }
800 if !matches!(steps.last(), Some(Step::Head { .. })) {
801 return Err(Error::Plan("the bundle's last step is not a head".into()));
802 }
803 Ok(IrEmbedder {
804 npu: Npu::open()?,
805 store,
806 steps,
807 head_bt,
808 model: m.param("model").ok().map(str::to_string),
809 in_c,
810 in_h,
811 in_w,
812 embed_dim: embed_dim.unwrap_or(0),
813 })
814 }
815
816 pub fn input_dims(&self) -> (usize, usize, usize) {
818 (self.in_c, self.in_h, self.in_w)
819 }
820
821 pub fn embed_dim(&self) -> usize {
823 self.embed_dim
824 }
825
826 pub fn model(&self) -> Option<&str> {
828 self.model.as_deref()
829 }
830
831 pub fn num_conv_dispatches(&self) -> usize {
833 self.steps
834 .iter()
835 .filter(|s| matches!(s, Step::Conv(_)))
836 .count()
837 }
838
839 pub fn embed(&mut self, chw: &[f32]) -> Result<Vec<f32>, Error> {
847 let plane = self.in_h * self.in_w;
848 if chw.is_empty() || chw.len() % plane != 0 {
849 return Err(Error::Input(format!(
850 "length {} is not a multiple of h*w = {plane}",
851 chw.len()
852 )));
853 }
854 let ch = chw.len() / plane;
855 if ch > self.in_c {
856 return Err(Error::Input(format!(
857 "{ch} channels, but the bundle's stem takes at most {}",
858 self.in_c
859 )));
860 }
861 let mut tiled = vec![0u16; self.in_h * (self.in_c / 8) * self.in_w * 8];
862 for c in 0..ch {
863 for y in 0..self.in_h {
864 for x in 0..self.in_w {
865 tiled[tidx(self.in_c, self.in_w, c, y, x)] =
866 f32_to_bf16(chw[(c * self.in_h + y) * self.in_w + x]);
867 }
868 }
869 }
870 self.run(Some(&tiled))
871 }
872
873 pub fn embed_recorded(&mut self) -> Result<Vec<f32>, Error> {
876 self.run(None)
877 }
878
879 fn run(&mut self, input: Option<&[u16]>) -> Result<Vec<f32>, Error> {
880 let Self {
881 npu,
882 store,
883 steps,
884 head_bt,
885 ..
886 } = self;
887 forward(npu, store, steps, head_bt, input)
888 }
889}
890
891fn forward(
897 npu: &mut Npu,
898 store: &Store,
899 steps: &[Step],
900 head_bt: &HashMap<String, Vec<f32>>,
901 input: Option<&[u16]>,
902) -> Result<Vec<f32>, Error> {
903 let mut bufs: HashMap<u32, Buf> = HashMap::new();
904 let mut acc: HashMap<u32, Vec<f32>> = HashMap::new();
905 let mut embed: Vec<f32> = Vec::new();
906
907 for step in steps {
908 match step {
909 Step::Input {
910 buf,
911 c,
912 h,
913 w,
914 tensor,
915 } => {
916 let (c, h, w) = (*c, *h, *w);
917 let v = match input {
918 Some(t) => {
919 if t.len() != h * (c / 8) * w * 8 {
920 return Err(Error::Input(format!(
921 "tiled input length {} != {}",
922 t.len(),
923 h * (c / 8) * w * 8
924 )));
925 }
926 t.to_vec()
927 }
928 None => store.bf16(tensor)?.to_vec(),
929 };
930 bufs.insert(*buf, Buf { v, c, h, w });
931 }
932 Step::Conv(cv) => {
933 let (oc, oh, ow, owp) = (cv.oc, cv.oh, cv.ow, cv.owp);
934 npu.ensure(&cv.ctx, &cv.xclbin, &cv.insts, &cv.kernel)?;
935 let inp = slice_groups_wpad(&bufs[&cv.input], cv.c0, cv.icg / 8, cv.wp);
936 let part = npu.run(&cv.ctx, &inp, store.bf16(&cv.wt)?, oc * oh * owp)?;
937 let a = acc
938 .entry(cv.out)
939 .or_insert_with(|| vec![0f32; oc * oh * owp]);
940 if cv.first {
941 a.iter_mut().for_each(|x| *x = 0.0);
942 }
943 for (dst, &p) in a.iter_mut().zip(&part) {
944 *dst += bf16_to_f32(p);
945 }
946 if cv.last {
947 let mut v = vec![0u16; oh * (oc / 8) * ow * 8];
949 for y in 0..oh {
950 for g in 0..(oc / 8) {
951 for x in 0..ow {
952 for lane in 0..8 {
953 let si = (((y * (oc / 8)) + g) * owp + x) * 8 + lane;
954 let di = (((y * (oc / 8)) + g) * ow + x) * 8 + lane;
955 v[di] = f32_to_bf16(a[si]);
956 }
957 }
958 }
959 }
960 bufs.insert(
961 cv.out,
962 Buf {
963 v,
964 c: oc,
965 h: oh,
966 w: ow,
967 },
968 );
969 }
970 }
971 Step::Affine {
972 input,
973 out,
974 scale,
975 shift,
976 } => {
977 let r = affine_tiled(&bufs[input], store.bf16(scale)?, store.bf16(shift)?);
978 bufs.insert(*out, r);
979 }
980 Step::PreluBias { buf, alpha, bias } => {
981 let buf = bufs.get_mut(buf).expect("checked at load");
982 prelu_bias_tiled(buf, store.bf16(alpha)?, store.bf16(bias)?);
983 }
984 Step::Bias { buf, bias } => {
985 let buf = bufs.get_mut(buf).expect("checked at load");
986 bias_tiled(buf, store.bf16(bias)?);
987 }
988 Step::Subsample { input, out, stride } => {
989 let r = subsample_tiled(&bufs[input], *stride);
990 bufs.insert(*out, r);
991 }
992 Step::Add { a, b, out } => {
993 let r = add_tiled(&bufs[a], &bufs[b]);
994 bufs.insert(*out, r);
995 }
996 Step::Head {
997 input,
998 k,
999 n,
1000 bt,
1001 b2,
1002 } => {
1003 let (k, n) = (*k, *n);
1009 let flat = tiled_to_nchw_f32(&bufs[input]); let bt = &head_bt[bt]; let mut e = store.f32(b2)?.to_vec(); for i in 0..k {
1013 let f = flat[i];
1014 let row = &bt[i * n..(i + 1) * n];
1015 for j in 0..n {
1016 e[j] += f * row[j];
1017 }
1018 }
1019 embed = e;
1020 }
1021 }
1022 }
1023 Ok(embed)
1024}
1025
1026pub fn cli(net_name: &str) {
1039 let a: Vec<String> = std::env::args().collect();
1040 if a.len() < 2 {
1041 let prog = a
1042 .first()
1043 .map(|p| p.rsplit('/').next().unwrap_or(p).to_string())
1044 .unwrap_or_else(|| "run_ir".to_string());
1045 eprintln!("usage: {prog} <bundle_dir> [reps]");
1046 std::process::exit(2);
1047 }
1048 let dir = PathBuf::from(&a[1]);
1049 let reps: usize = a
1052 .get(2)
1053 .cloned()
1054 .or_else(|| std::env::var("NREPS").ok())
1055 .and_then(|s| s.parse().ok())
1056 .unwrap_or(1)
1057 .max(1);
1058 if let Err(e) = cli_run(net_name, &dir, reps) {
1059 eprintln!("{e}");
1060 std::process::exit(1);
1061 }
1062}
1063
1064fn cli_run(net_name: &str, dir: &Path, reps: usize) -> Result<(), Error> {
1065 use std::time::Instant;
1066
1067 let mut emb = IrEmbedder::load(dir)?;
1068 let mut embed = Vec::new();
1069 let mut cold = 0f64;
1070 let mut warm_sum = 0f64;
1071 for i in 0..reps {
1072 let t = Instant::now();
1073 embed = emb.embed_recorded()?;
1074 let dt = t.elapsed().as_secs_f64() * 1e3;
1075 if i == 0 {
1076 cold = dt;
1077 } else {
1078 warm_sum += dt;
1079 }
1080 }
1081
1082 println!(
1083 "{} forward: {} conv dispatches on the NPU + CPU head",
1084 emb.model().unwrap_or(net_name),
1085 emb.num_conv_dispatches()
1086 );
1087 println!("embedding: {} dims", embed.len());
1088 if reps == 1 {
1089 println!("latency: {cold:.0} ms");
1090 } else {
1091 println!(
1092 "latency: cold {:.0} ms, warm {:.0} ms (mean of {} reps; contexts resident)",
1093 cold,
1094 warm_sum / (reps - 1) as f64,
1095 reps - 1
1096 );
1097 }
1098
1099 if let Ok(exp) = emb.store.f32("ref.npu_embedding") {
1101 println!(
1102 "cosine(Rust, recorder embedding) = {:.5}",
1103 cosine(&embed, exp)
1104 );
1105 }
1106 if let Ok(chw) = emb.store.f32("ref.input_chw") {
1110 let chw = chw.to_vec();
1111 let e2 = emb.embed(&chw)?;
1112 println!(
1113 "cosine(embed() API, recorded replay) = {:.5}",
1114 cosine(&e2, &embed)
1115 );
1116 }
1117 if let Ok(r) = emb.store.f32("ref.cpu_embedding") {
1119 let cos = cosine(&embed, r);
1120 println!("cosine(Rust, CPU reference) = {cos:.5}");
1121 println!(
1122 "{}",
1123 if cos > 0.99 {
1124 "RESULT: MATCH (Rust embedding agrees with the CPU reference)"
1125 } else {
1126 "RESULT: embedding differs from the CPU reference"
1127 }
1128 );
1129 }
1130 Ok(())
1131}