use std::os::raw::c_int;
use std::sync::atomic::{AtomicU32, Ordering};
use crate::MlxCError;
use crate::compile::graph::{
MlxCompiledGraph, array_from_vector, graph_output_array, set_graph_output_vector,
};
use crate::{MlxArray, MlxBindingsContext, MlxStream, raw};
static BAKED_RMS_NORM_EPSILON_BITS: AtomicU32 = AtomicU32::new(0);
fn baked_rms_norm_epsilon() -> f32 {
f32::from_bits(BAKED_RMS_NORM_EPSILON_BITS.load(Ordering::Relaxed))
}
const APPLY_DUAL_HEAD_NORM_OPERATION: &str = "apply the compiled MLX dual head norm";
const COMPILE_DUAL_HEAD_NORM_OPERATION: &str = "compile the shapeless MLX dual head norm graph";
#[derive(Debug)]
pub struct MlxCompiledDualHeadNorm {
compiled_graph: MlxCompiledGraph,
}
impl MlxCompiledDualHeadNorm {
pub fn new(rms_norm_epsilon: f32) -> Result<Self, MlxCError> {
BAKED_RMS_NORM_EPSILON_BITS.store(rms_norm_epsilon.to_bits(), Ordering::Relaxed);
Ok(Self {
compiled_graph: MlxCompiledGraph::new(
build_dual_head_norm_graph,
COMPILE_DUAL_HEAD_NORM_OPERATION,
true,
)?,
})
}
}
impl MlxBindingsContext {
pub fn apply_compiled_dual_head_norm(
&self,
dual_head_norm: &MlxCompiledDualHeadNorm,
queries: &MlxArray,
keys: &MlxArray,
query_scale_weight: &MlxArray,
key_scale_weight: &MlxArray,
) -> Result<(MlxArray, MlxArray), MlxCError> {
let mut graph_outputs = dual_head_norm.compiled_graph.apply_multi(
&[queries, keys, query_scale_weight, key_scale_weight],
APPLY_DUAL_HEAD_NORM_OPERATION,
)?;
if graph_outputs.len() != 2 {
return Err(MlxCError {
operation: APPLY_DUAL_HEAD_NORM_OPERATION,
description: format!(
"compiled MLX dual head norm returned {} outputs, expected 2",
graph_outputs.len()
),
});
}
let normalized_keys = graph_outputs
.pop()
.expect("the length check proved two outputs");
let normalized_queries = graph_outputs
.pop()
.expect("the length check proved two outputs");
Ok((normalized_queries, normalized_keys))
}
}
unsafe extern "C" fn build_dual_head_norm_graph(
output_vector: *mut raw::mlx_vector_array,
input_vector: raw::mlx_vector_array,
) -> c_int {
if output_vector.is_null() || unsafe { raw::mlx_vector_array_size(input_vector) } != 4 {
return 1;
}
let queries = match array_from_vector(input_vector, 0) {
Ok(queries) => queries,
Err(get_status) => return get_status,
};
let keys = match array_from_vector(input_vector, 1) {
Ok(keys) => keys,
Err(get_status) => return get_status,
};
let query_scale_weight = match array_from_vector(input_vector, 2) {
Ok(query_scale_weight) => query_scale_weight,
Err(get_status) => return get_status,
};
let key_scale_weight = match array_from_vector(input_vector, 3) {
Ok(key_scale_weight) => key_scale_weight,
Err(get_status) => return get_status,
};
let gpu_stream = match MlxStream::default_gpu() {
Ok(gpu_stream) => gpu_stream,
Err(_) => return 1,
};
let normalized_queries = match graph_output_array(|output_array| {
unsafe {
raw::mlx_fast_rms_norm(
output_array,
queries.raw(),
query_scale_weight.raw(),
baked_rms_norm_epsilon(),
gpu_stream.raw(),
)
}
}) {
Ok(normalized_queries) => normalized_queries,
Err(build_status) => return build_status,
};
let normalized_keys = match graph_output_array(|output_array| {
unsafe {
raw::mlx_fast_rms_norm(
output_array,
keys.raw(),
key_scale_weight.raw(),
baked_rms_norm_epsilon(),
gpu_stream.raw(),
)
}
}) {
Ok(normalized_keys) => normalized_keys,
Err(build_status) => return build_status,
};
unsafe { set_graph_output_vector(output_vector, &[&normalized_queries, &normalized_keys]) }
}