pub mod compiler;
pub mod provision;
pub mod raw;
use crate::{Error, Result, execution::KernelContract};
use std::{path::Path, sync::Arc};
pub(crate) fn load_shim() -> Result<libloading::Library> {
let directory = provision::resolve()?;
load_shim_from(&directory)
}
fn load_shim_from(directory: &Path) -> Result<libloading::Library> {
if directory.join("component.json").is_file() {
provision::Manifest::load(directory.join("component.json"))?.verify(directory)?;
}
let path = directory.join("libhrx_npu.so.1");
let library = unsafe { libloading::Library::new(&path) }.map_err(|e| {
Error::from(e).context(format!(
"loading {}; run hrx prepare (or check HRX_NPU_RUNTIME_DIR)",
path.display()
))
})?;
let abi = unsafe { library.get::<unsafe extern "C" fn() -> u32>(b"hrx_npu_abi_version\0") }?;
if unsafe { abi() } != 1 {
return Err(Error::Unsupported("NPU shim ABI must be 1".into()));
}
Ok(library)
}
#[derive(Clone)]
pub struct NpuProgram {
pub(crate) inner: Arc<ProgramInner>,
pub(crate) budget: Option<crate::residency::MemoryBudget>,
}
pub(crate) struct ProgramInner {
pub context: raw::Context,
pub device: i32,
pub identity: String,
}
impl NpuProgram {
pub unsafe fn load(device: i32, path: impl AsRef<Path>) -> Result<Self> {
let path = std::fs::canonicalize(path)?;
let identity = crate::bundle::file_digest(&path)?;
type Programs = std::collections::BTreeMap<(i32, String), std::sync::Weak<ProgramInner>>;
static PROGRAMS: std::sync::OnceLock<std::sync::Mutex<Programs>> =
std::sync::OnceLock::new();
let mut programs = PROGRAMS
.get_or_init(Default::default)
.lock()
.map_err(|_| Error::Message("NPU program registry poisoned".into()))?;
if let Some(inner) = programs
.get(&(device, identity.clone()))
.and_then(std::sync::Weak::upgrade)
{
return Ok(Self {
inner,
budget: None,
});
}
programs.retain(|_, program| program.strong_count() != 0);
let path_str = path
.to_str()
.ok_or_else(|| Error::Message("xclbin path is not UTF-8".into()))?;
let context = unsafe { raw::Context::new(device, path_str) }.map_err(Error::Message)?;
if crate::bundle::file_digest(&path)? != identity {
return Err(Error::Message("xclbin changed while loading".into()));
}
let inner = Arc::new(ProgramInner {
context,
device,
identity: identity.clone(),
});
programs.insert((device, identity), Arc::downgrade(&inner));
Ok(Self {
inner,
budget: None,
})
}
pub fn identity(&self) -> &str {
&self.inner.identity
}
pub unsafe fn kernel(
&self,
instructions: &[u8],
contract: KernelContract,
) -> Result<NpuKernel> {
contract.validate()?;
if !contract.constants.is_empty()
|| instructions.is_empty()
|| !instructions.len().is_multiple_of(4)
|| instructions.len() / 4 > u32::MAX as usize
{
return Err(Error::Message(
"MLIR_AIE requires a nonempty u32 instruction stream and no extra scalar arguments"
.into(),
));
}
let context = &self.inner.context;
let reservation = self
.budget
.as_ref()
.map(|budget| budget.reserve(instructions.len()))
.transpose()?;
let insts = context
.alloc_bo_reserved(
instructions.len(),
raw::BoKind::Cacheable,
context.group_id(1).map_err(Error::Message)?,
reservation,
)
.map_err(Error::Message)?;
insts.write(instructions).map_err(Error::Message)?;
let groups = (0..contract.bindings.len())
.map(|i| context.group_id((i + 3) as i32).map_err(Error::Message))
.collect::<Result<Vec<_>>>()?;
Ok(NpuKernel {
inner: Arc::new(KernelInner {
program: self.clone(),
instructions: insts,
contract,
groups,
}),
})
}
}
#[derive(Clone)]
pub struct NpuKernel {
pub(crate) inner: Arc<KernelInner>,
}
pub(crate) struct KernelInner {
pub program: NpuProgram,
pub instructions: raw::Bo,
pub contract: KernelContract,
pub groups: Vec<i32>,
}
impl NpuKernel {
pub fn contract(&self) -> &KernelContract {
&self.inner.contract
}
}