use cubecl_core::{
WgpuCompilationOptions,
ir::{AddressType, UIntKind},
prelude::Visibility,
server::KernelArguments,
};
use cubecl_cpp::{
metal::{arch::MetalArchitecture, supported_cmma_combinations_metal},
shared::{MslComputeKernel, register_wmma_features},
};
use cubecl_ir::{
DeviceProperties, Type,
features::{AtomicUsage, Plane, TypeUsage},
};
use wgpu::{
DeviceDescriptor, Features, Limits,
hal::{self, Adapter, metal},
};
pub fn bindings(repr: &MslComputeKernel, args: &KernelArguments) -> (Vec<Visibility>, usize) {
let buffers = repr.buffers.iter().map(|it| {
if cubecl_server::memory_management::EXCLUSIVE_MEMORY_ONLY {
*it
} else {
Visibility::ReadWrite
}
});
let uniform = args.info.dynamic_metadata_offset >= args.info.data.len();
let info_vis = (!args.info.data.is_empty()).then_some(match uniform {
true => Visibility::Uniform,
false => Visibility::Read,
});
(buffers.chain(info_vis).collect(), 0)
}
pub async fn request_metal_device(adapter: &wgpu::Adapter) -> (wgpu::Device, wgpu::Queue) {
try_request_metal_device(adapter)
.await
.expect("Unable to request Metal device")
}
pub async fn try_request_metal_device(
adapter: &wgpu::Adapter,
) -> Result<(wgpu::Device, wgpu::Queue), crate::WgpuInitError> {
let limits = adapter.limits();
let features = adapter
.features()
.difference(Features::MAPPABLE_PRIMARY_BUFFERS);
unsafe {
let hal_adapter = adapter.as_hal::<hal::api::Metal>().ok_or_else(|| {
crate::WgpuInitError::InvalidConfiguration {
message: "requesting a native Metal device requires a Metal adapter".into(),
}
})?;
request_device(adapter, &hal_adapter, features, limits)
}
}
fn request_device(
wgpu_adapter: &wgpu::Adapter,
adapter: &metal::Adapter,
features: Features,
limits: Limits,
) -> Result<(wgpu::Device, wgpu::Queue), crate::WgpuInitError> {
let memory_hints = wgpu::MemoryHints::MemoryUsage;
let device = unsafe {
adapter
.open(features, &limits, &memory_hints)
.map_err(|err| crate::WgpuInitError::RequestDevice {
message: err.to_string(),
})?
};
let descriptor = DeviceDescriptor {
label: None,
required_features: features,
required_limits: limits,
memory_hints,
trace: wgpu::Trace::Off,
experimental_features: unsafe { wgpu::ExperimentalFeatures::enabled() },
};
unsafe {
wgpu_adapter
.create_device_from_hal(device, &descriptor)
.map_err(|err| crate::WgpuInitError::RequestDevice {
message: err.to_string(),
})
}
}
#[cfg(target_vendor = "apple")]
pub fn register_metal_features(
adapter: &wgpu::Adapter,
props: &mut DeviceProperties,
comp_options: &mut WgpuCompilationOptions,
) -> bool {
const LAMBDA_CANARY: &str = r#"
#include <metal_stdlib>
using namespace metal;
kernel void __canary__(device float* out [[buffer(0)]]) {
auto f = [](float x) -> float { return x * 2.0; };
out[0] = f(1.0);
}
"#;
let features = adapter.features();
unsafe {
use objc2::rc::autoreleasepool;
use objc2_foundation::NSString;
use objc2_metal::{MTLCompileOptions, MTLDevice, MTLGPUFamily, MTLLanguageVersion};
let Some(adapter) = adapter.as_hal::<hal::api::Metal>() else {
return false;
};
let raw = adapter.raw_device();
props
.memory
.set_max_memory(raw.recommendedMaxWorkingSetSize());
let has_required_family = raw.supportsFamily(MTLGPUFamily::Metal3)
|| raw.supportsFamily(MTLGPUFamily::Apple7)
|| raw.supportsFamily(MTLGPUFamily::Mac2);
let support = MslSupport::new(has_required_family, || {
autoreleasepool(|_| {
let canary = NSString::from_str(LAMBDA_CANARY);
let options = MTLCompileOptions::new();
options.setLanguageVersion(MTLLanguageVersion::Version3_2);
raw.newLibraryWithSource_options_error(&canary, Some(&options))
.map(|_| ())
.map_err(|err| format!("{err:?}"))
})
});
match support {
MslSupport::Supported => {
comp_options.supports_msl_compiler = true;
register_features(&adapter, props, features, comp_options);
true
}
MslSupport::FamilyTooOld => {
log::warn!(
"The device predates the Metal 3 GPU family native MSL needs; it runs WGSL instead."
);
false
}
MslSupport::CompilerTooOld { reason } => {
log::warn!(
"Device can support native MSL, but Metal compiler version is too old. Upgrading to 3.2 or higher is recommended. MSL 3.2 canary compilation failed: {reason}"
);
false
}
}
}
}
#[derive(Debug, PartialEq, Eq)]
enum MslSupport {
Supported,
FamilyTooOld,
CompilerTooOld {
reason: String,
},
}
impl MslSupport {
fn new(has_required_family: bool, compile_canary: impl FnOnce() -> Result<(), String>) -> Self {
if !has_required_family {
return Self::FamilyTooOld;
}
match compile_canary() {
Ok(()) => Self::Supported,
Err(reason) => Self::CompilerTooOld { reason },
}
}
}
#[cfg(not(target_vendor = "apple"))]
pub fn register_metal_features(
_: &wgpu::Adapter,
_: &mut DeviceProperties,
_: &mut WgpuCompilationOptions,
) -> bool {
false
}
fn register_features(
_adapter: &metal::Adapter,
props: &mut DeviceProperties,
_features: Features,
_comp_options: &mut WgpuCompilationOptions,
) {
register_types(props);
register_cmma(props);
props.features.alignment = true;
props.features.device_memory_scope = true;
props.features.plane.insert(Plane::Ops);
props.features.plane.insert(Plane::Sync);
}
fn register_types(props: &mut DeviceProperties) {
use cubecl_core::ir::{ElemType, FloatKind, IntKind};
props.register_address_type(AddressType::U32);
props.register_address_type(AddressType::U64);
let types = [
ElemType::Index,
ElemType::UInt(UIntKind::U8),
ElemType::UInt(UIntKind::U16),
ElemType::UInt(UIntKind::U32),
ElemType::UInt(UIntKind::U64),
ElemType::Int(IntKind::I8),
ElemType::Int(IntKind::I16),
ElemType::Int(IntKind::I32),
ElemType::Int(IntKind::I64),
ElemType::Float(FloatKind::F16),
ElemType::Float(FloatKind::BF16),
ElemType::Float(FloatKind::F32),
ElemType::Bool,
];
for ty in types {
props.register_type_usage(ty, TypeUsage::all());
}
for ty in [
FloatKind::E4M3,
FloatKind::E5M2,
FloatKind::UE8M0,
FloatKind::E2M1x2,
] {
props.register_type_usage(
ElemType::Float(ty),
TypeUsage::Conversion | TypeUsage::Buffer,
);
}
for ty in [ElemType::Int(IntKind::I32), ElemType::UInt(UIntKind::U32)] {
props.register_atomic_type_usage(Type::atomic(ty), AtomicUsage::all());
}
props.register_atomic_type_usage(
Type::atomic(ElemType::Float(FloatKind::F32)),
AtomicUsage::Add | AtomicUsage::LoadStore,
);
}
fn register_cmma(props: &mut DeviceProperties) {
let combinations = supported_cmma_combinations_metal(&MetalArchitecture::Metal3);
register_wmma_features(combinations, props);
}
#[cfg(all(test, target_vendor = "apple"))]
mod tests {
use super::*;
#[test]
fn a_device_of_the_family_with_a_current_compiler_runs_msl() {
assert_eq!(MslSupport::new(true, || Ok(())), MslSupport::Supported);
}
#[test]
fn a_device_before_the_family_stays_on_wgsl_without_compiling() {
let support = MslSupport::new(false, || panic!("the canary must not compile"));
assert_eq!(support, MslSupport::FamilyTooOld);
}
#[test]
fn a_compiler_before_msl_3_2_stays_on_wgsl() {
let support = MslSupport::new(true, || Err("no lambdas".into()));
assert_eq!(
support,
MslSupport::CompilerTooOld {
reason: "no lambdas".into()
}
);
}
}