use std::collections::HashMap;
use std::fmt::Debug;
use std::hash::Hash;
use std::sync::{Mutex, OnceLock};
use sha2::{Digest, Sha256};
use crate::Error;
use crate::profile::Profile;
use crate::tier::{ComputeCapability, PtxTier};
pub trait KernelArea: Copy + Eq + Hash + Debug + Send + Sync + 'static {
const PROFILE: &'static Profile;
fn name(self) -> &'static str;
fn variants(self) -> AreaPtx;
}
#[macro_export]
macro_rules! embed_tier {
($ptx:path, $cubin:path; [$($feature:literal),+], $stem:literal, [$($arch:literal),+]) => {{
#[cfg(any($(feature = $feature),+))]
const CUBINS: &[$crate::EmbeddedCubin] = &[$($crate::EmbeddedCubin {
arch: $crate::ComputeCapability::new($arch / 10, $arch % 10),
bytes: const { $cubin(concat!($stem, ".sm_", stringify!($arch), ".cubin")) },
}),+];
#[cfg(any($(feature = $feature),+))]
let ptx = Some($crate::EmbeddedPtx {
text: const { $ptx(concat!($stem, ".ptx")) },
cubins: CUBINS,
});
#[cfg(not(any($(feature = $feature),+)))]
let ptx = None;
ptx
}};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ArtifactHash([u8; 32]);
impl ArtifactHash {
pub fn of(bytes: &[u8]) -> Self {
Self(Sha256::digest(bytes).into())
}
pub const fn from_hex(text: &str) -> Self {
const fn digit(byte: u8) -> u8 {
match byte {
b'0'..=b'9' => byte - b'0',
b'a'..=b'f' => byte - b'a' + 10,
_ => panic!("hash must use lowercase hexadecimal"),
}
}
assert!(
text.len() == 64,
"hash must contain 64 hexadecimal characters"
);
let mut bytes = [0; 32];
let mut index = 0;
while index < bytes.len() {
bytes[index] =
digit(text.as_bytes()[index * 2]) * 16 + digit(text.as_bytes()[index * 2 + 1]);
index += 1;
}
Self(bytes)
}
pub const fn const_eq(self, other: Self) -> bool {
let mut index = 0;
while index < self.0.len() {
if self.0[index] != other.0[index] {
return false;
}
index += 1;
}
true
}
}
impl std::fmt::Display for ArtifactHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for byte in self.0 {
write!(f, "{byte:02x}")?;
}
Ok(())
}
}
struct EmbeddedArtifact(&'static [u8]);
impl EmbeddedArtifact {
fn sha256(self) -> ArtifactHash {
static HASHES: OnceLock<Mutex<HashMap<(usize, usize), ArtifactHash>>> = OnceLock::new();
let mut hashes = HASHES
.get_or_init(Mutex::default)
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*hashes
.entry((self.0.as_ptr() as usize, self.0.len()))
.or_insert_with(|| ArtifactHash::of(self.0))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LoadedArtifact {
Cubin {
arch: ComputeCapability,
sha256: ArtifactHash,
},
PtxJit {
sha256: ArtifactHash,
},
}
#[derive(Debug, Clone, Copy)]
pub struct EmbeddedCubin {
pub arch: ComputeCapability,
pub bytes: &'static [u8],
}
impl EmbeddedCubin {
pub fn sha256(self) -> ArtifactHash {
EmbeddedArtifact(self.bytes).sha256()
}
}
#[derive(Debug, Clone, Copy)]
pub struct EmbeddedPtx {
pub text: &'static str,
pub cubins: &'static [EmbeddedCubin],
}
impl EmbeddedPtx {
pub fn sha256(self) -> ArtifactHash {
EmbeddedArtifact(self.text.as_bytes()).sha256()
}
pub fn cubin(self, device: ComputeCapability) -> Option<EmbeddedCubin> {
self.cubins
.iter()
.copied()
.find(|cubin| cubin.arch == device)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct AreaPtx {
variants: [Option<EmbeddedPtx>; PtxTier::COUNT],
}
impl AreaPtx {
pub const EMPTY: Self = Self {
variants: [None; PtxTier::COUNT],
};
pub const fn baseline(sm75: Option<EmbeddedPtx>) -> Self {
Self::EMPTY.with(PtxTier::BASELINE, sm75)
}
pub const fn with(mut self, tier: PtxTier, ptx: Option<EmbeddedPtx>) -> Self {
self.variants[tier.index()] = ptx;
self
}
#[cfg(feature = "test-support")]
pub fn fixture(variants: &[(PtxTier, &'static str)]) -> Self {
variants.iter().fold(Self::EMPTY, |area, (tier, text)| {
area.with(*tier, Some(EmbeddedPtx { text, cubins: &[] }))
})
}
pub fn iter(&self) -> impl Iterator<Item = (PtxTier, &'static str)> {
PtxTier::ALL
.iter()
.filter_map(|tier| Some((*tier, self.embedded(*tier)?.text)))
}
pub fn embedded(&self, tier: PtxTier) -> Option<EmbeddedPtx> {
self.variants[tier.index()]
}
pub fn runnable_tier(self, limit: PtxTier, device: ComputeCapability) -> Option<PtxTier> {
PtxTier::ALL.iter().copied().rev().find(|tier| {
*tier <= limit && tier.min_capability() <= device && self.embedded(*tier).is_some()
})
}
pub fn driver_request<K: KernelArea>(
self,
area: K,
limit: PtxTier,
device: ComputeCapability,
) -> Option<ModuleRequest<K>> {
let tier = self.runnable_tier(limit, device)?;
let ptx = self.embedded(tier)?;
let artifact = ptx.cubin(device).map_or_else(
|| LoadedArtifact::PtxJit {
sha256: ptx.sha256(),
},
|cubin| LoadedArtifact::Cubin {
arch: device,
sha256: cubin.sha256(),
},
);
Some(ModuleRequest::new(area, tier, artifact))
}
pub fn select(&self, limit: PtxTier) -> Option<(PtxTier, &'static str)> {
self.iter().filter(|(tier, _)| *tier <= limit).last()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ModuleRequest<K> {
area: K,
tier: PtxTier,
artifact: LoadedArtifact,
}
impl<K: KernelArea> ModuleRequest<K> {
pub const fn new(area: K, tier: PtxTier, artifact: LoadedArtifact) -> Self {
if let LoadedArtifact::Cubin { arch, .. } = artifact {
assert!(
arch.at_least(tier.min_capability()),
"a cubin cannot be older than its PTX tier"
);
}
Self {
area,
tier,
artifact,
}
}
pub const fn area(self) -> K {
self.area
}
pub const fn tier(self) -> PtxTier {
self.tier
}
pub const fn artifact(self) -> LoadedArtifact {
self.artifact
}
pub fn check_cached(self, cached: Self) -> Result<(), Error> {
if self != cached {
return Err(Error::ArtifactUnavailable {
module: self.area.name(),
artifact: self.artifact,
});
}
Ok(())
}
pub fn diagnostic_request(self, force_jit: bool) -> Result<Self, Error> {
if !force_jit {
return Ok(self);
}
let embedded =
self.area
.variants()
.embedded(self.tier)
.ok_or(Error::ArtifactUnavailable {
module: self.area.name(),
artifact: self.artifact,
})?;
Ok(self.ptx_jit(ArtifactHash::of(embedded.text.as_bytes())))
}
pub const fn ptx_jit(self, sha256: ArtifactHash) -> Self {
Self::new(self.area, self.tier, LoadedArtifact::PtxJit { sha256 })
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum ArtifactLoadError<E> {
Unavailable,
Driver(E),
}
pub fn load_artifact<T, E>(
requested: LoadedArtifact,
cubin: Option<EmbeddedCubin>,
ptx_sha256: ArtifactHash,
load_cubin: impl FnOnce(&[u8]) -> Result<T, E>,
load_ptx: impl FnOnce() -> Result<T, E>,
) -> Result<(T, LoadedArtifact), ArtifactLoadError<E>> {
let loaded = match requested {
LoadedArtifact::PtxJit { sha256 } if sha256 == ptx_sha256 => load_ptx(),
LoadedArtifact::Cubin { arch, sha256 } => {
let Some(cubin) = cubin.filter(|cubin| cubin.arch == arch && cubin.sha256() == sha256)
else {
return Err(ArtifactLoadError::Unavailable);
};
load_cubin(cubin.bytes)
}
_ => return Err(ArtifactLoadError::Unavailable),
};
loaded
.map(|loaded| (loaded, requested))
.map_err(ArtifactLoadError::Driver)
}
#[cfg(test)]
mod tests {
use super::{
ArtifactHash, ArtifactLoadError, EmbeddedArtifact, EmbeddedCubin, LoadedArtifact,
load_artifact,
};
use crate::ComputeCapability;
#[test]
fn embedded_hash_cache_preserves_content_and_subview_identity() {
static FIRST: &[u8] = b"first artifact";
static SECOND: &[u8] = b"other artifact";
for bytes in [FIRST, SECOND, &FIRST[..5], FIRST, SECOND] {
assert_eq!(EmbeddedArtifact(bytes).sha256(), ArtifactHash::of(bytes));
}
assert_ne!(
EmbeddedArtifact(FIRST).sha256(),
EmbeddedArtifact(&FIRST[..5]).sha256()
);
}
#[test]
fn hex_pins_round_trip_and_compare_in_const_context() {
const PIN: ArtifactHash = ArtifactHash::from_hex(
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
);
assert_eq!(PIN, ArtifactHash::of(b"hello"));
assert_eq!(
PIN.to_string(),
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
);
assert!(PIN.const_eq(ArtifactHash::of(b"hello")));
assert!(!PIN.const_eq(ArtifactHash::of(b"other")));
}
#[test]
fn pinned_loading_never_substitutes_artifacts() {
let hash = ArtifactHash::of(b"ptx");
let jit = LoadedArtifact::PtxJit { sha256: hash };
for device in [ComputeCapability::new(8, 9), ComputeCapability::new(12, 0)] {
let cubin = EmbeddedCubin {
arch: device,
bytes: b"cubin",
};
let binary = LoadedArtifact::Cubin {
arch: device,
sha256: ArtifactHash::of(cubin.bytes),
};
let result = load_artifact(
binary,
Some(cubin),
hash,
|bytes| Ok::<_, &str>(bytes.to_vec()),
|| panic!("cubin request cannot JIT"),
)
.unwrap();
assert_eq!(result, (cubin.bytes.to_vec(), binary));
assert_eq!(
load_artifact::<(), _>(
binary,
Some(cubin),
hash,
|_| Err("driver refusal"),
|| panic!("refused cubin must not JIT")
),
Err(ArtifactLoadError::Driver("driver refusal"))
);
assert_eq!(
load_artifact::<(), &str>(
binary,
None,
hash,
|_| panic!("missing cubin"),
|| panic!("missing cubin must not JIT")
),
Err(ArtifactLoadError::Unavailable)
);
assert_eq!(
load_artifact(
jit,
Some(cubin),
hash,
|_| panic!("PTX request cannot load cubin"),
|| Ok::<_, &str>("jit")
),
Ok(("jit", jit))
);
assert_eq!(
load_artifact::<(), _>(
jit,
Some(cubin),
hash,
|_| panic!("refused PTX cannot load cubin"),
|| Err("PTX rejected")
),
Err(ArtifactLoadError::Driver("PTX rejected"))
);
}
let wrong = LoadedArtifact::PtxJit {
sha256: ArtifactHash::of(b"stale ptx"),
};
assert_eq!(
load_artifact::<(), &str>(
wrong,
None,
hash,
|_| panic!("wrong hash"),
|| panic!("wrong hash")
),
Err(ArtifactLoadError::Unavailable)
);
}
}