use super::{Compiler, CompilerOptions, Kernels, Specialization};
pub use crate::execution::Access;
use crate::{
Buffer, Constants, Device, Error, GraphExec, Kernel, Node, Result, Stream, Target,
benchmark::percentile,
dependency_frontier::{Frontier, Use},
};
use std::{
collections::HashMap,
sync::atomic::{AtomicU64, Ordering},
time::Instant,
};
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,
}
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 open(index: i32) -> Result<Self> {
let device = Device::open(index)?;
let options = CompilerOptions {
target: device.target().clone(),
..CompilerOptions::default()
};
let compiler = Compiler::shared(None, options)?;
Self::with_compiler(device, compiler)
}
pub fn open_for(index: i32, expected_target: &str) -> Result<Self> {
let session = Self::open(index)?;
if session.target().as_str() != expected_target {
return Err(Error::Message(format!(
"model kernels require {expected_target}, found {}",
session.target().as_str()
)));
}
Ok(session)
}
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()
)));
}
let stream = device.stream()?;
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 });
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)?;
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.graph()?;
let mut frontier = Frontier::default();
let mut nodes: Vec<Node> = Vec::with_capacity(prepared.len());
for (index, command) in prepared.iter().enumerate() {
let dependencies = frontier.dependencies(index, command.uses());
let after = dependencies
.into_iter()
.map(|dependency| nodes[dependency])
.collect::<Vec<_>>();
let node = match command {
PreparedCommand::Fill { region, value } => {
graph.fill(&after, self.view(*region)?, *value)?
}
PreparedCommand::Dispatch {
kernel,
constants,
grid,
bindings,
} => {
let kernel = self.kernel(*kernel)?;
let views = self.views(bindings)?;
unsafe {
graph.dispatch(
&after,
kernel,
*grid,
kernel.info().workgroup_size,
constants,
&views,
)?
}
}
};
nodes.push(node);
}
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()
}
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(())
}
}
impl PreparedCommand {
fn uses(&self) -> Vec<Use> {
match self {
Self::Fill { region, .. } => vec![Use::new(*region, Access::Write)],
Self::Dispatch { bindings, .. } => bindings
.iter()
.map(|binding| Use::new(binding.region, binding.access))
.collect(),
}
}
}
impl Use {
fn new(region: Region, access: Access) -> Self {
Self {
allocation: region.allocation,
range: region.offset..region.offset + region.bytes,
access,
}
}
}
#[derive(Clone, Copy, Debug, serde::Serialize)]
pub struct ForwardTimings {
pub samples: usize,
pub graph: Distribution,
pub direct: Distribution,
}
#[derive(Clone, Copy, Debug, serde::Serialize)]
pub struct Distribution {
pub median_ms: f64,
pub p95_ms: f64,
}
impl Distribution {
fn from_samples(mut samples: Vec<f64>) -> Self {
samples.sort_by(f64::total_cmp);
Self {
median_ms: percentile(&samples, 50),
p95_ms: percentile(&samples, 95),
}
}
}
#[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());
assert_eq!(distribution.median_ms, 50.0);
assert_eq!(distribution.p95_ms, 95.0);
}
}