212 lines
7.3 KiB
Rust
212 lines
7.3 KiB
Rust
//! Residency at actual submission boundaries. This is not yet the reference
|
|
//! dependency/fence encoder; dispatch access roles and that port remain open.
|
|
use super::super::*;
|
|
use super::allocator::Allocator;
|
|
use std::cell::Cell;
|
|
use std::rc::Rc;
|
|
use std::sync::Arc;
|
|
|
|
pub(super) struct Submission {
|
|
allocator: Arc<Allocator>,
|
|
queue: NonNull<c_void>,
|
|
attached: Cell<usize>,
|
|
#[cfg(test)]
|
|
commits: Cell<usize>,
|
|
}
|
|
|
|
impl Submission {
|
|
pub(super) fn new(allocator: Arc<Allocator>) -> Result<Rc<Self>, String> {
|
|
let queue = NonNull::new(unsafe { ds4_gpu_mtplx_retain_command_queue() })
|
|
.ok_or("Metal command queue is not initialized")?;
|
|
let owner = Rc::new(Self {
|
|
allocator,
|
|
queue,
|
|
attached: Cell::new(0),
|
|
#[cfg(test)]
|
|
commits: Cell::new(0),
|
|
});
|
|
owner.attach(); // Reference CommandEncoder attaches existing sets at creation.
|
|
Ok(owner)
|
|
}
|
|
|
|
fn attach(&self) {
|
|
let mut attached = self.attached.get();
|
|
unsafe {
|
|
self.allocator
|
|
.residency
|
|
.attach_new_sets(self.queue.as_ptr(), &mut attached)
|
|
};
|
|
self.attached.set(attached);
|
|
}
|
|
|
|
pub(super) fn begin(self: &Rc<Self>) -> Result<SubmissionCommands, String> {
|
|
if unsafe { ds4_gpu_mtplx_command_queue() } != self.queue.as_ptr() {
|
|
return Err("Metal submission owner belongs to a different queue".into());
|
|
}
|
|
unsafe extern "C" fn before_commit(context: *mut c_void, queue: *mut c_void) {
|
|
// The scope owns an Rc at this stable address. The native hook is
|
|
// thread-local and synchronous; Submission/Rc cannot cross threads.
|
|
let owner = unsafe { &*context.cast::<Submission>() };
|
|
assert_eq!(queue, owner.queue.as_ptr());
|
|
owner.attach();
|
|
#[cfg(test)]
|
|
owner.commits.set(owner.commits.get() + 1);
|
|
}
|
|
let context = Rc::as_ptr(self).cast_mut().cast();
|
|
check(
|
|
unsafe { ds4_gpu_mtplx_submission_hook(context, Some(before_commit)) },
|
|
"registering Metal submission owner",
|
|
)?;
|
|
match Commands::begin() {
|
|
Ok(commands) => Ok(SubmissionCommands {
|
|
commands: Some(commands),
|
|
owner: self.clone(),
|
|
}),
|
|
Err(error) => {
|
|
unsafe { ds4_gpu_mtplx_submission_hook(context, None) };
|
|
Err(error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for Submission {
|
|
fn drop(&mut self) {
|
|
unsafe { ds4_gpu_mtplx_queue_free(self.queue.as_ptr()) };
|
|
}
|
|
}
|
|
|
|
pub(super) struct SubmissionCommands {
|
|
commands: Option<Commands>,
|
|
owner: Rc<Submission>,
|
|
}
|
|
|
|
impl SubmissionCommands {
|
|
pub(super) fn flush(&mut self) -> Result<(), String> {
|
|
self.commands.as_mut().unwrap().flush()
|
|
}
|
|
|
|
pub(super) fn eval_boundary(&mut self) -> Result<(), String> {
|
|
self.commands.as_mut().unwrap().eval_boundary()
|
|
}
|
|
|
|
pub(super) fn finish(mut self) -> Result<(), String> {
|
|
self.commands.take().unwrap().finish()
|
|
}
|
|
}
|
|
|
|
impl Drop for SubmissionCommands {
|
|
fn drop(&mut self) {
|
|
// Drop/finish drains native work while the hook and its owner still live.
|
|
drop(self.commands.take());
|
|
let context = Rc::as_ptr(&self.owner).cast_mut().cast();
|
|
assert_ne!(unsafe { ds4_gpu_mtplx_submission_hook(context, None) }, 0);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn mtplx_all_native_command_commits_pass_the_submission_hook() {
|
|
let source = include_str!("../../../../native/metal/ds4_metal.m");
|
|
assert_eq!(source.matches("[cb commit]").count(), 1);
|
|
assert!(!source.contains("[g_batch_cb commit]"));
|
|
for line in source.lines().filter(|line| line.contains(" commit]")) {
|
|
assert!(
|
|
line.contains("[cb commit]")
|
|
|| line.contains("[g_model_residency_set commit]")
|
|
|| line.contains("[residency_set commit]")
|
|
|| line.contains("id<MTLResidencySet>)set commit]"),
|
|
"unreviewed native commit bypass: {line}"
|
|
);
|
|
}
|
|
let helper = source
|
|
.split_once("static void ds4_gpu_commit_command_buffer(")
|
|
.unwrap()
|
|
.1
|
|
.split_once("static void ds4_gpu_stream_expert_cache_note_owned_created")
|
|
.unwrap()
|
|
.0;
|
|
assert!(
|
|
helper
|
|
.find("g_mtplx_before_commit(g_mtplx_submission_context")
|
|
.unwrap()
|
|
< helper.find("[cb commit]").unwrap()
|
|
);
|
|
assert!(helper.contains("cb.commandQueue == g_queue"));
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires Apple Metal, no model weights"]
|
|
fn mtplx_submission_attaches_at_flush_eval_finish_and_drop() {
|
|
configure_sources().unwrap();
|
|
let _context = Context::open_qwen(0).unwrap();
|
|
let allocator = Allocator::new().unwrap();
|
|
allocator.residency.set_max_per_set(8 << 20);
|
|
allocator.set_wired_limit(128 << 20).unwrap();
|
|
let owner = Submission::new(allocator.clone()).unwrap();
|
|
assert_eq!(owner.attached.get(), allocator.residency.num_sets());
|
|
let input = allocator.allocate(16).unwrap().unwrap();
|
|
let output = allocator.allocate(16).unwrap().unwrap();
|
|
let expected = [0x80, 0x3f].repeat(8);
|
|
input.write(0, &expected).unwrap();
|
|
let copy = || {
|
|
assert!(
|
|
super::qsa_dynamic_copy_rows([&input, &output], [8, 8], [1, 8], [None, None], [8; 2])
|
|
.unwrap()
|
|
.is_empty()
|
|
)
|
|
};
|
|
let mut commands = owner.begin().unwrap();
|
|
assert!(
|
|
owner.begin().is_err(),
|
|
"nested scopes must not replace the outer hook"
|
|
);
|
|
copy();
|
|
let buffers = (0..8)
|
|
.map(|_| allocator.allocate(5 << 20).unwrap().unwrap())
|
|
.collect::<Vec<_>>();
|
|
assert!(owner.attached.get() < allocator.residency.num_sets());
|
|
commands.flush().unwrap();
|
|
assert_eq!(owner.commits.get(), 1);
|
|
assert_eq!(owner.attached.get(), allocator.residency.num_sets());
|
|
let big = allocator.allocate(32 << 20).unwrap().unwrap();
|
|
assert!(owner.attached.get() < allocator.residency.num_sets());
|
|
commands.eval_boundary().unwrap();
|
|
assert_eq!(owner.commits.get(), 2);
|
|
assert_eq!(owner.attached.get(), allocator.residency.num_sets());
|
|
commands.finish().unwrap();
|
|
assert_eq!(owner.commits.get(), 3);
|
|
let mut actual = [0; 16];
|
|
output.read(0, &mut actual).unwrap();
|
|
assert_eq!(actual.as_slice(), expected);
|
|
drop((buffers, big));
|
|
|
|
// Drop submits with the owner still registered. Reuse the same queue cursor.
|
|
let commands = owner.begin().unwrap();
|
|
copy();
|
|
drop(commands);
|
|
assert_eq!(owner.commits.get(), 4);
|
|
let commands = owner.begin().unwrap();
|
|
assert_eq!(unsafe { ds4_gpu_end_commands() }, 1);
|
|
assert_eq!(
|
|
owner.commits.get(),
|
|
5,
|
|
"native-internal submission bypasses no hook"
|
|
);
|
|
copy(); // With no open batch, the native dispatch commits synchronously.
|
|
assert_eq!(owner.commits.get(), 6);
|
|
drop(commands);
|
|
assert_eq!(
|
|
owner.commits.get(),
|
|
6,
|
|
"no duplicate commit during scope cleanup"
|
|
);
|
|
|
|
// The legacy scope neither inherits the owner nor calls an expired context.
|
|
let commands = Commands::begin().unwrap();
|
|
copy();
|
|
commands.finish().unwrap();
|
|
assert_eq!(owner.commits.get(), 6);
|
|
output.read(0, &mut actual).unwrap();
|
|
assert_eq!(actual.as_slice(), expected);
|
|
}
|