use crate::loom::{Compiler, CompilerOptions, Kernels, Specialization};
use crate::{
Access, Buffer, Constants, Device, Error, GraphExec, Kernel, Result, Stream, Target,
benchmark::Distribution,
};
use std::{
collections::{BTreeMap, BTreeSet, HashMap},
sync::atomic::{AtomicU64, Ordering},
time::Instant,
};
#[path = "model/definition.rs"]
mod definition;
pub use definition::{ModelDefinition, ModelFragment};
static NEXT_SESSION_ID: AtomicU64 = AtomicU64::new(1);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Region {
session: u64,
allocation: usize,
offset: usize,
bytes: usize,
}
impl Region {
#[must_use]
pub fn len(self) -> usize {
self.bytes
}
#[must_use]
pub fn is_empty(self) -> bool {
self.bytes == 0
}
#[must_use]
pub fn offset(self) -> usize {
self.offset
}
pub fn slice(self, offset: usize, bytes: usize) -> Result<Self> {
let end = offset
.checked_add(bytes)
.ok_or_else(|| Error::Message("model region size overflow".into()))?;
if end > self.bytes {
return Err(Error::Message("model region exceeds its parent".into()));
}
Ok(Self {
offset: self
.offset
.checked_add(offset)
.ok_or_else(|| Error::Message("model region offset overflow".into()))?,
bytes,
..self
})
}
#[must_use]
pub fn read(self) -> Binding {
Binding {
region: self,
access: Access::Read,
}
}
#[must_use]
pub fn write(self) -> Binding {
Binding {
region: self,
access: Access::Write,
}
}
#[must_use]
pub fn read_write(self) -> Binding {
Binding {
region: self,
access: Access::ReadWrite,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Binding {
pub region: Region,
pub access: Access,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct KernelId {
session: u64,
index: usize,
}
#[derive(Clone, Debug)]
pub enum Arguments {
Indices(Vec<u32>),
Packed(Box<Constants>),
}
impl Arguments {
pub fn indices(values: impl IntoIterator<Item = u32>) -> Self {
Self::Indices(values.into_iter().collect())
}
#[must_use]
pub fn none() -> Self {
Self::Packed(Box::new(Constants::new()))
}
#[must_use]
pub fn packed(constants: Constants) -> Self {
Self::Packed(Box::new(constants))
}
}
#[derive(Clone, Debug)]
pub struct Dispatch {
pub kernel: KernelId,
pub arguments: Arguments,
pub grid: [u32; 3],
pub bindings: Vec<Binding>,
}
impl Dispatch {
pub fn indices(
kernel: KernelId,
values: impl IntoIterator<Item = u32>,
grid: [u32; 3],
bindings: Vec<Binding>,
) -> Self {
Self {
kernel,
arguments: Arguments::indices(values),
grid,
bindings,
}
}
}
#[derive(Clone, Debug)]
pub enum Command {
Fill {
region: Region,
value: u8,
},
Dispatch(Dispatch),
}
#[derive(Clone)]
enum PreparedCommand {
Fill {
region: Region,
value: u8,
},
Dispatch {
kernel: KernelId,
constants: Box<Constants>,
grid: [u32; 3],
bindings: Vec<Binding>,
},
}
struct PreparedGraph {
executable: GraphExec,
commands: Vec<PreparedCommand>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Placement {
Device,
Shared,
}
struct Allocation {
buffer: Buffer,
placement: Placement,
immutable: bool,
}
pub struct ModelSession {
id: u64,
stream: Stream,
allocations: Vec<Allocation>,
kernels: Vec<Kernel>,
cache: Kernels,
graphs: HashMap<usize, PreparedGraph>,
readback: Option<Buffer>,
failed: bool,
}
impl ModelSession {
pub fn in_context(context: &crate::inference::ModelContext) -> Result<Self> {
let mut stream = Device::open(context.runtime().gpu()?.index())?.stream()?;
if let Some(budget) = context.runtime().memory_budget() {
stream = stream.with_memory_budget(budget.clone());
}
Self::with_stream(stream, context.compiler()?)
}
pub fn open(index: i32) -> Result<Self> {
let device = Device::open(index)?;
let stream = device.stream()?;
let options = CompilerOptions::for_stream(&stream);
let compiler = Compiler::shared(None, options)?;
Self::with_stream(stream, compiler)
}
pub fn open_for(index: i32, expected_target: &str) -> Result<Self> {
let device = Device::open_for(index, expected_target)?;
let stream = device.stream()?;
let compiler = Compiler::for_stream(None, &stream)?;
Self::with_stream(stream, compiler)
}
pub fn with_compiler(device: Device, compiler: Compiler) -> Result<Self> {
if compiler.target() != device.target() {
return Err(Error::Message(format!(
"compiler target {} does not match device target {}",
compiler.target().as_str(),
device.target().as_str()
)));
}
Self::with_stream(device.stream()?, compiler)
}
fn with_stream(stream: Stream, compiler: Compiler) -> Result<Self> {
if compiler.target() != stream.target() {
return Err(Error::Message(format!(
"compiler target {} does not match device target {}",
compiler.target().as_str(),
stream.target().as_str()
)));
}
Ok(Self {
id: NEXT_SESSION_ID.fetch_add(1, Ordering::Relaxed),
stream,
allocations: Vec::new(),
kernels: Vec::new(),
cache: Kernels::new(compiler),
graphs: HashMap::new(),
readback: None,
failed: false,
})
}
#[must_use]
pub fn target(&self) -> &Target {
self.stream.target()
}
pub fn allocate(&mut self, bytes: usize) -> Result<Region> {
self.allocate_with(bytes, Placement::Device)
}
pub fn allocate_shared(&mut self, bytes: usize) -> Result<Region> {
self.allocate_with(bytes, Placement::Shared)
}
fn allocate_with(&mut self, bytes: usize, placement: Placement) -> Result<Region> {
self.usable()?;
let buffer = match placement {
Placement::Device => self.stream.allocate(bytes)?,
Placement::Shared => self.stream.allocate_shared(bytes)?,
};
let allocation = self.allocations.len();
self.allocations.push(Allocation {
buffer,
placement,
immutable: false,
});
Ok(Region {
session: self.id,
allocation,
offset: 0,
bytes,
})
}
pub fn weight(&mut self, bytes: &[u8]) -> Result<Region> {
let region = self.allocate(bytes.len())?;
self.upload(region, bytes)?;
self.allocations[region.allocation].immutable = true;
Ok(region)
}
pub unsafe fn compile(
&mut self,
specifications: &[(&str, Specialization)],
) -> Result<Vec<KernelId>> {
self.usable()?;
let pending = specifications
.iter()
.map(|(source, specialization)| self.cache.request(source, specialization))
.collect::<Result<Vec<_>>>()?;
unsafe { self.cache.build(&self.stream)? };
let mut loaded = Vec::with_capacity(pending.len());
for request in pending {
loaded.push(unsafe { request.resolve(&self.stream)? }.clone());
}
self.synchronize()?;
let first = self.kernels.len();
self.kernels.extend(loaded);
Ok((first..self.kernels.len())
.map(|index| KernelId {
session: self.id,
index,
})
.collect())
}
#[must_use]
pub fn is_recorded(&self, key: usize) -> bool {
self.graphs.contains_key(&key)
}
pub unsafe fn record(&mut self, key: usize, commands: &[Command]) -> Result<()> {
self.usable()?;
if commands.is_empty() {
return Err(Error::Message("cannot record an empty model graph".into()));
}
if self.graphs.contains_key(&key) {
return Err(Error::Message(format!(
"model graph key {key} is already recorded"
)));
}
let prepared = self.prepare(commands)?;
let mut graph = self.stream.access_graph()?;
for command in &prepared {
match command {
PreparedCommand::Fill { region, value } => {
graph.fill(self.view(*region)?, *value)?;
}
PreparedCommand::Dispatch {
kernel,
constants,
grid,
bindings,
} => {
let kernel = self.kernel(*kernel)?;
let views = bindings
.iter()
.map(|binding| Ok(self.view(binding.region)?.access(binding.access)))
.collect::<Result<Vec<_>>>()?;
unsafe {
graph.dispatch(
kernel,
*grid,
kernel.info().workgroup_size,
constants,
&views,
)?
};
}
}
}
let executable = graph.finish()?;
self.graphs.insert(
key,
PreparedGraph {
executable,
commands: prepared,
},
);
Ok(())
}
pub fn upload(&mut self, region: Region, bytes: &[u8]) -> Result<()> {
self.usable()?;
if bytes.len() > region.bytes {
return Err(Error::Message("upload exceeds model region".into()));
}
self.validate_region(region)?;
let allocation = &self.allocations[region.allocation].buffer;
let view = allocation
.try_slice(region.offset, region.bytes)?
.slice(0, bytes.len())?;
let result = self.stream.upload(view, bytes);
if result.is_err() {
self.failed = true;
}
result
}
pub fn replay(&mut self, key: usize) -> Result<()> {
self.usable()?;
let graph = self
.graphs
.get_mut(&key)
.ok_or_else(|| Error::Message(format!("model graph key {key} is not recorded")))?;
let result = self.stream.launch(&mut graph.executable);
if result.is_err() {
self.failed = true;
}
result
}
pub fn synchronize(&mut self) -> Result<()> {
self.usable()?;
let result = self.stream.synchronize();
if result.is_err() {
self.failed = true;
}
result
}
pub fn reserve_readback(&mut self, bytes: usize) -> Result<()> {
self.usable()?;
if self
.readback
.as_ref()
.is_none_or(|buffer| buffer.bytes() < bytes)
{
self.readback = Some(self.stream.allocate_shared(bytes)?);
}
Ok(())
}
pub fn read(&mut self, region: Region, bytes: &mut [u8]) -> Result<()> {
self.read_many(&mut [(region, bytes)])
}
pub fn read_many(&mut self, outputs: &mut [(Region, &mut [u8])]) -> Result<()> {
self.usable()?;
let mut total = 0usize;
let mut all_shared = true;
for (region, output) in outputs.iter() {
self.validate_region(*region)?;
if output.len() > region.bytes {
return Err(Error::Message("readback exceeds model region".into()));
}
total = total
.checked_add(output.len())
.ok_or_else(|| Error::Message("readback size overflow".into()))?;
all_shared &= self.allocations[region.allocation].placement == Placement::Shared;
}
if all_shared {
self.synchronize()?;
for (region, output) in outputs.iter_mut() {
if output.is_empty() {
continue;
}
let pointer = self.allocations[region.allocation]
.buffer
.device_ptr()?
.cast::<u8>();
if pointer.is_null() {
self.failed = true;
return Err(Error::Message("null shared model pointer".into()));
}
unsafe {
std::ptr::copy_nonoverlapping(
pointer.add(region.offset),
output.as_mut_ptr(),
output.len(),
);
}
}
return Ok(());
}
self.reserve_readback(total)?;
{
let readback = self.readback.as_ref().expect("reserved above");
let mut offset = 0;
for (region, output) in outputs.iter() {
if !output.is_empty() {
let source = self.allocations[region.allocation]
.buffer
.try_slice(region.offset, output.len())?;
if let Err(error) = self
.stream
.copy(readback.try_slice(offset, output.len())?, source)
{
self.failed = true;
return Err(error);
}
}
offset += output.len();
}
}
self.synchronize()?;
if total == 0 {
return Ok(());
}
let readback = self.readback.as_ref().expect("reserved above");
let pointer = readback.device_ptr()?.cast::<u8>();
if pointer.is_null() {
self.failed = true;
return Err(Error::Message("null model readback pointer".into()));
}
let mut offset = 0;
for (_, output) in outputs.iter_mut() {
if !output.is_empty() {
unsafe {
std::ptr::copy_nonoverlapping(
pointer.add(offset),
output.as_mut_ptr(),
output.len(),
);
}
}
offset += output.len();
}
Ok(())
}
pub fn benchmark(&mut self, key: usize, samples: usize) -> Result<ForwardTimings> {
if samples < 10 {
return Err(Error::Message(
"use at least ten model timing samples".into(),
));
}
self.usable()?;
let commands = self
.graphs
.get(&key)
.ok_or_else(|| Error::Message(format!("model graph key {key} is not recorded")))?
.commands
.clone();
let mut times = [Vec::with_capacity(samples), Vec::with_capacity(samples)];
for iteration in 0..samples + 10 {
for mode in [iteration % 2, 1 - iteration % 2] {
let start = Instant::now();
if mode == 0 {
self.replay(key)?;
} else {
let result = unsafe { self.execute_direct(&commands) };
if result.is_err() {
self.failed = true;
}
result?;
}
self.synchronize()?;
if iteration >= 10 {
times[mode].push(start.elapsed().as_secs_f64() * 1_000.0);
}
}
}
let [graph, direct] = times;
Ok(ForwardTimings {
samples,
graph: Distribution::from_samples(graph)?,
direct: Distribution::from_samples(direct)?,
})
}
pub fn profile_commands(&mut self, key: usize, samples: usize) -> Result<Vec<Distribution>> {
if samples == 0 {
return Err(Error::Message(
"use at least one command timing sample".into(),
));
}
self.usable()?;
let commands = self
.graphs
.get(&key)
.ok_or_else(|| Error::Message(format!("model graph key {key} is not recorded")))?
.commands
.clone();
let mut distributions = Vec::with_capacity(commands.len());
for command in commands {
let warmup = unsafe { self.execute_direct(std::slice::from_ref(&command)) };
if warmup.is_err() {
self.failed = true;
}
warmup?;
self.synchronize()?;
let mut times = Vec::with_capacity(samples);
for _ in 0..samples {
let start = Instant::now();
let result = unsafe { self.execute_direct(std::slice::from_ref(&command)) };
if result.is_err() {
self.failed = true;
}
result?;
self.synchronize()?;
times.push(start.elapsed().as_secs_f64() * 1_000.0);
}
distributions.push(Distribution::from_samples(times)?);
}
Ok(distributions)
}
fn usable(&self) -> Result<()> {
if self.failed {
Err(Error::Message(
"model session is unusable after a GPU failure".into(),
))
} else {
Ok(())
}
}
fn validate_region(&self, region: Region) -> Result<()> {
if region.session != self.id {
return Err(Error::Message(
"model region belongs to another session".into(),
));
}
let allocation = self
.allocations
.get(region.allocation)
.ok_or_else(|| Error::Message("invalid model allocation".into()))?;
let end = region
.offset
.checked_add(region.bytes)
.ok_or_else(|| Error::Message("model region size overflow".into()))?;
if end > allocation.buffer.bytes() {
return Err(Error::Message("model region exceeds its allocation".into()));
}
Ok(())
}
fn view(&self, region: Region) -> Result<crate::View<'_>> {
self.validate_region(region)?;
self.allocations[region.allocation]
.buffer
.try_slice(region.offset, region.bytes)
}
fn views<'a>(&'a self, bindings: &[Binding]) -> Result<Vec<crate::View<'a>>> {
bindings
.iter()
.map(|binding| self.view(binding.region))
.collect()
}
pub fn launch_config(
&self,
id: KernelId,
workload_bits: &[u64],
) -> Result<crate::loom::LaunchConfig> {
self.usable()?;
self.kernel(id)?.launch_config(workload_bits)
}
fn kernel(&self, id: KernelId) -> Result<&Kernel> {
if id.session != self.id {
return Err(Error::Message(
"model kernel belongs to another session".into(),
));
}
self.kernels
.get(id.index)
.ok_or_else(|| Error::Message("invalid model kernel".into()))
}
fn prepare(&self, commands: &[Command]) -> Result<Vec<PreparedCommand>> {
commands
.iter()
.map(|command| match command {
Command::Fill { region, value } => {
self.validate_region(*region)?;
if region.is_empty() {
return Err(Error::Message("cannot record an empty model fill".into()));
}
Ok(PreparedCommand::Fill {
region: *region,
value: *value,
})
}
Command::Dispatch(dispatch) => {
let kernel = self.kernel(dispatch.kernel)?;
for binding in &dispatch.bindings {
self.validate_region(binding.region)?;
}
let constants = match &dispatch.arguments {
Arguments::Indices(values) => Box::new(Constants::indices(kernel, values)?),
Arguments::Packed(constants) => constants.clone(),
};
Ok(PreparedCommand::Dispatch {
kernel: dispatch.kernel,
constants,
grid: dispatch.grid,
bindings: dispatch.bindings.clone(),
})
}
})
.collect()
}
unsafe fn execute_direct(&self, commands: &[PreparedCommand]) -> Result<()> {
for command in commands {
match command {
PreparedCommand::Fill { region, value } => {
self.stream.fill(self.view(*region)?, *value)?;
}
PreparedCommand::Dispatch {
kernel,
constants,
grid,
bindings,
} => {
let kernel = self.kernel(*kernel)?;
let views = self.views(bindings)?;
unsafe {
self.stream.dispatch(
kernel,
*grid,
kernel.info().workgroup_size,
constants,
&views,
)?;
}
}
}
}
Ok(())
}
}
#[derive(Clone, Copy, Debug)]
struct ScratchLifetime {
bytes: usize,
first: usize,
last: usize,
}
#[derive(Clone, Debug, Default)]
pub struct ScratchPlanner<K> {
values: BTreeMap<K, ScratchLifetime>,
}
impl<K: Clone + Ord> ScratchPlanner<K> {
#[must_use]
pub fn new() -> Self {
Self {
values: BTreeMap::new(),
}
}
pub fn insert(&mut self, key: K, bytes: usize, first: usize, last: usize) -> Result<()> {
if bytes == 0 {
return Err(Error::Message("scratch values must be nonempty".into()));
}
if first > last {
return Err(Error::Message(
"scratch lifetime ends before it begins".into(),
));
}
if self
.values
.insert(key, ScratchLifetime { bytes, first, last })
.is_some()
{
return Err(Error::Message("duplicate scratch value".into()));
}
Ok(())
}
pub fn finish(self) -> ScratchPlan<K> {
let mut values = self.values.into_iter().collect::<Vec<_>>();
values.sort_by(|(left_key, left), (right_key, right)| {
(left.first, std::cmp::Reverse(left.bytes), left_key).cmp(&(
right.first,
std::cmp::Reverse(right.bytes),
right_key,
))
});
let mut active = Vec::<(usize, usize)>::new();
let mut free = BTreeSet::<usize>::new();
let mut slots = Vec::<usize>::new();
let mut assignments = BTreeMap::new();
for (key, lifetime) in values {
active.retain(|&(last, slot)| {
if last < lifetime.first {
free.insert(slot);
false
} else {
true
}
});
let slot = free
.iter()
.copied()
.filter(|&slot| slots[slot] >= lifetime.bytes)
.min_by_key(|&slot| slots[slot])
.map_or_else(
|| {
slots.push(lifetime.bytes);
slots.len() - 1
},
|slot| {
free.remove(&slot);
slot
},
);
assignments.insert(key, slot);
active.push((lifetime.last, slot));
}
ScratchPlan { assignments, slots }
}
}
#[derive(Clone, Debug)]
pub struct ScratchPlan<K> {
assignments: BTreeMap<K, usize>,
slots: Vec<usize>,
}
impl<K: Ord> ScratchPlan<K> {
#[must_use]
pub fn slots(&self) -> &[usize] {
&self.slots
}
pub fn slot(&self, key: &K) -> Result<usize> {
self.assignments
.get(key)
.copied()
.ok_or_else(|| Error::Message("scratch value was not planned".into()))
}
#[must_use]
pub fn assignments(&self) -> &BTreeMap<K, usize> {
&self.assignments
}
}
#[derive(Clone, Copy, Debug, serde::Serialize)]
pub struct ForwardTimings {
pub samples: usize,
pub graph: Distribution,
pub direct: Distribution,
}
#[cfg(test)]
mod tests {
use super::*;
use std::ops::Range;
fn region(allocation: usize, range: Range<usize>) -> Region {
Region {
session: 1,
allocation,
offset: range.start,
bytes: range.end - range.start,
}
}
#[test]
fn region_slices_are_relative_and_checked() {
let whole = region(2, 100..200);
let slice = whole.slice(25, 50).unwrap();
assert_eq!(slice.offset(), 125);
assert_eq!(slice.len(), 50);
assert!(whole.slice(101, 0).is_err());
assert!(whole.slice(usize::MAX, 2).is_err());
}
#[test]
fn distributions_use_nearest_rank_quantiles() {
let distribution = Distribution::from_samples((1..=100).map(f64::from).collect()).unwrap();
assert_eq!(distribution.median_ms, 50.0);
assert_eq!(distribution.p95_ms, 95.0);
}
#[test]
fn scratch_slots_reuse_only_after_the_last_use() {
let mut planner = ScratchPlanner::new();
planner.insert("a", 8, 0, 1).unwrap();
planner.insert("b", 16, 1, 2).unwrap();
planner.insert("c", 4, 2, 3).unwrap();
planner.insert("d", 7, 3, 3).unwrap();
let plan = planner.finish();
assert_ne!(plan.slot(&"a").unwrap(), plan.slot(&"b").unwrap());
assert_eq!(plan.slot(&"a").unwrap(), plan.slot(&"c").unwrap());
assert_eq!(plan.slot(&"b").unwrap(), plan.slot(&"d").unwrap());
}
}