mirror of
https://github.com/hanzoai/musetalk-bench.git
synced 2026-11-03 01:49:22 +00:00
Per-conv MPSGraph::new()+compile was a net loss on the UNets many small convs (64->88ms). Caching the compiled graph+placeholders per (shape,dtype,params): UNet back to 62ms (baseline parity), FULL-FAST 5.38->10.42 fps, full-VAE 1.04->2.24 fps. cosine still 1.000000. Co-authored-by: Hanzo Dev <[email protected]>
309 lines
11 KiB
Rust
309 lines
11 KiB
Rust
use crate::{DType, Result};
|
|
|
|
#[cfg(feature = "ug")]
|
|
use hanzo_metal_kernels::metal::ComputePipeline;
|
|
use hanzo_metal_kernels::{
|
|
metal::{
|
|
BlitCommandEncoder, Buffer, BufferMap, Commands, ComputeCommandEncoder, Device,
|
|
MTLResourceOptions,
|
|
},
|
|
Kernels,
|
|
};
|
|
use objc2_foundation::NSURL;
|
|
use objc2_metal::{MTLCaptureDescriptor, MTLCaptureDestination, MTLCaptureManager};
|
|
|
|
use std::path::Path;
|
|
use std::sync::{Arc, Mutex, RwLock};
|
|
|
|
use super::MetalError;
|
|
|
|
/// Unique identifier for metal devices.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
pub struct DeviceId(usize);
|
|
|
|
impl DeviceId {
|
|
pub(crate) fn new() -> Self {
|
|
// https://users.rust-lang.org/t/idiomatic-rust-way-to-generate-unique-id/33805
|
|
use std::sync::atomic;
|
|
static COUNTER: atomic::AtomicUsize = atomic::AtomicUsize::new(1);
|
|
Self(COUNTER.fetch_add(1, atomic::Ordering::Relaxed))
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct MetalDevice {
|
|
/// Unique identifier, the registryID is not sufficient as it identifies the GPU rather than
|
|
/// the device itself.
|
|
pub(crate) id: DeviceId,
|
|
|
|
/// Raw metal device: <https://developer.apple.com/documentation/metal/mtldevice?language=objc>
|
|
pub(crate) device: Device,
|
|
|
|
pub(crate) commands: Arc<RwLock<Commands>>,
|
|
|
|
/// Simple allocator struct.
|
|
/// The buffers are stored in size buckets since ML tends to use similar shapes over and over.
|
|
/// We store the buffers in [`Arc`] because it's much faster than Obj-c internal ref counting
|
|
/// (could be linked to FFI communication overhead).
|
|
///
|
|
/// Whenever a buffer has a strong_count==1, we can reuse it, it means it was dropped in the
|
|
/// graph calculation, and only we the allocator kept a reference to it, therefore it's free
|
|
/// to be reused. However, in order for this to work, we need to guarantee the order of
|
|
/// operation, so that this buffer is not being used by another kernel at the same time.
|
|
/// Arc is the CPU reference count, it doesn't mean anything on the GPU side of things.
|
|
///
|
|
/// Whenever we actually allocate a new buffer, we make a full sweep to clean up unused buffers
|
|
/// (strong_count = 1).
|
|
pub(crate) buffers: Arc<RwLock<BufferMap>>,
|
|
|
|
/// Simple keeper struct to keep track of the already compiled kernels so we can reuse them.
|
|
/// Heavily used by [`hanzo_metal_kernels`]
|
|
pub(crate) kernels: Arc<Kernels>,
|
|
/// Seed for random number generation.
|
|
pub(crate) seed: Arc<Mutex<Buffer>>,
|
|
/// Last seed value set on this device.
|
|
pub(crate) seed_value: Arc<RwLock<u64>>,
|
|
/// Lazily-created dedicated command queue for the MPSGraph conv path (see `mps_conv.rs`).
|
|
/// Kept separate from `commands` so MPSGraph work doesn't interleave with candle's encoders;
|
|
/// correctness is via `wait_until_completed()` before each MPSGraph run.
|
|
pub(crate) mps_queue: Arc<std::sync::OnceLock<hanzo_metal_kernels::metal::CommandQueue>>,
|
|
/// Compiled-MPSGraph cache keyed by conv shape/dtype (see `mps_conv.rs`). MPSGraph build +
|
|
/// compile is not free; caching keeps the UNet's many small convs fast.
|
|
pub(crate) mps_conv_cache: Arc<RwLock<super::mps_conv::ConvGraphCache>>,
|
|
}
|
|
|
|
// Resource options used for creating buffers. Shared storage mode allows both CPU and GPU to access the buffer.
|
|
pub const RESOURCE_OPTIONS: MTLResourceOptions =
|
|
objc2_metal::MTLResourceOptions(MTLResourceOptions::StorageModeShared.bits());
|
|
//| MTLResourceOptions::HazardTrackingModeUntracked.bits(),
|
|
//);
|
|
|
|
// Resource options used for `new_private_buffer`. This uses `private` where supported.
|
|
#[cfg(target_os = "ios")]
|
|
pub const PRIVATE_RESOURCE_OPTIONS: MTLResourceOptions = MTLResourceOptions::StorageModeShared;
|
|
#[cfg(not(target_os = "ios"))]
|
|
pub const PRIVATE_RESOURCE_OPTIONS: MTLResourceOptions = MTLResourceOptions::StorageModePrivate;
|
|
|
|
impl std::fmt::Debug for MetalDevice {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "MetalDevice({:?})", self.id)
|
|
}
|
|
}
|
|
|
|
impl std::ops::Deref for MetalDevice {
|
|
type Target = Device;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.device
|
|
}
|
|
}
|
|
|
|
impl MetalDevice {
|
|
#[cfg(all(feature = "ug", not(target_arch = "wasm32"), not(target_os = "ios")))]
|
|
pub fn compile(
|
|
&self,
|
|
func_name: &'static str,
|
|
kernel: hanzo_ug::lang::ssa::Kernel,
|
|
) -> Result<ComputePipeline> {
|
|
let mut buf = vec![];
|
|
hanzo_ug::metal::code_gen::gen(&mut buf, func_name, &kernel)?;
|
|
let metal_code = String::from_utf8(buf)?;
|
|
let lib = self
|
|
.device
|
|
.new_library_with_source(&metal_code, None)
|
|
.map_err(MetalError::from)?;
|
|
let func = lib
|
|
.get_function(func_name, None)
|
|
.map_err(MetalError::from)?;
|
|
let pl = self
|
|
.device
|
|
.new_compute_pipeline_state_with_function(&func)
|
|
.map_err(MetalError::from)?;
|
|
Ok(pl)
|
|
}
|
|
|
|
pub fn id(&self) -> DeviceId {
|
|
self.id
|
|
}
|
|
|
|
pub fn metal_device(&self) -> &Device {
|
|
&self.device
|
|
}
|
|
|
|
fn drop_unused_buffers(&self) -> Result<()> {
|
|
let mut buffers = self.buffers.write().map_err(MetalError::from)?;
|
|
for subbuffers in buffers.values_mut() {
|
|
let newbuffers = subbuffers
|
|
.iter()
|
|
.filter(|s| Arc::strong_count(*s) > 1)
|
|
.map(Arc::clone)
|
|
.collect();
|
|
*subbuffers = newbuffers;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn command_encoder(&self) -> Result<ComputeCommandEncoder> {
|
|
let commands = self.commands.write().map_err(MetalError::from)?;
|
|
let (flush, command_encoder) = commands.command_encoder().map_err(MetalError::from)?;
|
|
if flush {
|
|
self.drop_unused_buffers()?
|
|
}
|
|
Ok(command_encoder)
|
|
}
|
|
|
|
pub fn blit_command_encoder(&self) -> Result<BlitCommandEncoder> {
|
|
let commands = self.commands.write().map_err(MetalError::from)?;
|
|
let (flush, command_encoder) = commands.blit_command_encoder().map_err(MetalError::from)?;
|
|
if flush {
|
|
self.drop_unused_buffers()?
|
|
}
|
|
Ok(command_encoder)
|
|
}
|
|
|
|
pub fn wait_until_completed(&self) -> Result<()> {
|
|
let commands = self.commands.write().map_err(MetalError::from)?;
|
|
commands.wait_until_completed().map_err(MetalError::from)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn kernels(&self) -> &Kernels {
|
|
&self.kernels
|
|
}
|
|
|
|
pub fn device(&self) -> &Device {
|
|
&self.device
|
|
}
|
|
|
|
/// Dedicated, lazily-created command queue for the MPSGraph conv path. Created once per device
|
|
/// (queue creation is not free) and reused across convs.
|
|
pub fn mps_command_queue(
|
|
&self,
|
|
) -> Result<hanzo_metal_kernels::metal::CommandQueue> {
|
|
if let Some(q) = self.mps_queue.get() {
|
|
return Ok(q.clone());
|
|
}
|
|
let q = self.device.new_command_queue().map_err(MetalError::from)?;
|
|
// Race-safe: if another thread set it first, keep theirs.
|
|
let _ = self.mps_queue.set(q);
|
|
Ok(self.mps_queue.get().unwrap().clone())
|
|
}
|
|
|
|
/// Creates a new buffer (not necessarily zeroed).
|
|
pub fn new_buffer(
|
|
&self,
|
|
element_count: usize,
|
|
dtype: DType,
|
|
_name: &str,
|
|
) -> Result<Arc<Buffer>> {
|
|
let size = element_count * dtype.size_in_bytes();
|
|
self.allocate_buffer(size)
|
|
}
|
|
|
|
/// Creates a new private buffer (not necessarily zeroed).
|
|
///
|
|
/// This is intentionally not in the Metal buffer pool to allow the efficient implementation of persistent buffers.
|
|
pub fn new_private_buffer(
|
|
&self,
|
|
element_count: usize,
|
|
dtype: DType,
|
|
_name: &str,
|
|
) -> Result<Arc<Buffer>> {
|
|
let size = element_count * dtype.size_in_bytes();
|
|
let buffer = self
|
|
.device
|
|
.new_buffer(size, PRIVATE_RESOURCE_OPTIONS)
|
|
.map_err(MetalError::from)?;
|
|
Ok(Arc::new(buffer))
|
|
}
|
|
|
|
/// Creates a new buffer from data.
|
|
///
|
|
/// Does not require synchronization, as [newBufferWithBytes](https://developer.apple.com/documentation/metal/mtldevice/1433429-newbufferwithbytes)
|
|
/// allocates the buffer and copies over the existing data before returning the MTLBuffer.
|
|
pub fn new_buffer_with_data<T>(&self, data: &[T]) -> Result<Arc<Buffer>> {
|
|
let size = core::mem::size_of_val(data);
|
|
let new_buffer = self
|
|
.device
|
|
.new_buffer_with_data(data.as_ptr().cast(), size, RESOURCE_OPTIONS)
|
|
.map_err(MetalError::from)?;
|
|
let mut buffers = self.buffers.write().map_err(MetalError::from)?;
|
|
|
|
let subbuffers = buffers.entry(size).or_insert(vec![]);
|
|
|
|
let new_buffer = Arc::new(new_buffer);
|
|
subbuffers.push(new_buffer.clone());
|
|
Ok(new_buffer)
|
|
}
|
|
|
|
pub fn allocate_zeros(&self, size_in_bytes: usize) -> Result<Arc<Buffer>> {
|
|
let buffer = self.allocate_buffer(size_in_bytes)?;
|
|
let blit = self.blit_command_encoder()?;
|
|
blit.set_label("zeros");
|
|
blit.fill_buffer(&buffer, (0, buffer.length()), 0);
|
|
blit.end_encoding();
|
|
Ok(buffer)
|
|
}
|
|
|
|
/// The critical allocator algorithm
|
|
pub fn allocate_buffer(&self, size: usize) -> Result<Arc<Buffer>> {
|
|
let mut buffers = self.buffers.write().map_err(MetalError::from)?;
|
|
if let Some(b) = find_available_buffer(size, &buffers) {
|
|
// Cloning also ensures we increment the strong count
|
|
return Ok(b.clone());
|
|
}
|
|
let size = buf_size(size);
|
|
let subbuffers = buffers.entry(size).or_insert(vec![]);
|
|
|
|
let new_buffer = self
|
|
.device
|
|
.new_buffer(size, RESOURCE_OPTIONS)
|
|
.map_err(MetalError::from)?;
|
|
let new_buffer = Arc::new(new_buffer);
|
|
subbuffers.push(new_buffer.clone());
|
|
Ok(new_buffer)
|
|
}
|
|
|
|
/// Create a metal GPU capture trace on [`path`].
|
|
pub fn capture<P: AsRef<Path>>(&self, path: P) -> Result<()> {
|
|
let capture = unsafe { MTLCaptureManager::sharedCaptureManager() };
|
|
let descriptor = MTLCaptureDescriptor::new();
|
|
descriptor.setDestination(MTLCaptureDestination::GPUTraceDocument);
|
|
descriptor.set_capture_device(self.device().as_ref());
|
|
// The [set_output_url] call requires an absolute path so we convert it if needed.
|
|
if path.as_ref().is_absolute() {
|
|
let url = NSURL::from_file_path(path);
|
|
descriptor.setOutputURL(url.as_deref());
|
|
} else {
|
|
let path = std::env::current_dir()?.join(path);
|
|
let url = NSURL::from_file_path(path);
|
|
descriptor.setOutputURL(url.as_deref());
|
|
}
|
|
|
|
capture
|
|
.startCaptureWithDescriptor_error(&descriptor)
|
|
.map_err(|e| MetalError::from(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn buf_size(size: usize) -> usize {
|
|
size.saturating_sub(1).next_power_of_two()
|
|
}
|
|
|
|
fn find_available_buffer(size: usize, buffers: &BufferMap) -> Option<Arc<Buffer>> {
|
|
let mut best_buffer: Option<&Arc<Buffer>> = None;
|
|
let mut best_buffer_size = usize::MAX;
|
|
for (buffer_size, subbuffers) in buffers.iter() {
|
|
if buffer_size >= &size && buffer_size < &best_buffer_size {
|
|
for sub in subbuffers {
|
|
if Arc::strong_count(sub) == 1 {
|
|
best_buffer = Some(sub);
|
|
best_buffer_size = *buffer_size;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
best_buffer.cloned()
|
|
}
|