Skip to main content

mlx_c_rust/compile/
gated_delta_rate_activation.rs

1//! Dual-output compiled composite for the gated-delta rate activations.
2//!
3//! The sigmoid over the update logits and the complete decay formula both
4//! consume the two pre-activations of the fused rate projection, and each ran
5//! as its own launch in every gated-delta layer of every decode step. One
6//! multi-output compiled replay produces both, reading each input once. The
7//! per-element arithmetic is the same primitive sequence the two separate
8//! launches ran — sigmoid untouched, and the decay chain identical to
9//! [`crate::compile::elementwise_graphs`]'s single-output graph — so the
10//! replay matches the composed path bit for bit; the fusion parity contract
11//! in the prism family's graphics-processor tests replays both routes over
12//! adversarial inputs to prove it (issue #1366: decode is bound by launches
13//! per token, and this removes one launch from every gated-delta layer).
14
15use std::os::raw::c_int;
16
17use crate::MlxCError;
18use crate::compile::graph::{
19    MlxCompiledGraph, array_from_vector, graph_output_array, set_graph_output_vector,
20};
21use crate::{MlxArray, MlxBindingsContext, MlxStream, raw};
22
23const APPLY_GATED_DELTA_RATE_ACTIVATION_OPERATION: &str =
24    "apply the compiled MLX gated-delta rate activation";
25const COMPILE_GATED_DELTA_RATE_ACTIVATION_OPERATION: &str =
26    "compile the shapeless MLX gated-delta rate activation graph";
27
28/// Owns the compiled dual-output rate-activation graph.
29///
30/// Output order is fixed by the builder: the sigmoid update rates first, the
31/// decay factors second. Callers consume them by that order.
32#[derive(Debug)]
33pub struct MlxCompiledGatedDeltaRateActivation {
34    compiled_graph: MlxCompiledGraph,
35}
36
37impl MlxCompiledGatedDeltaRateActivation {
38    /// Compiles the shapeless dual-output rate-activation graph.
39    pub fn new() -> Result<Self, MlxCError> {
40        Ok(Self {
41            compiled_graph: MlxCompiledGraph::new(
42                build_gated_delta_rate_activation_graph,
43                COMPILE_GATED_DELTA_RATE_ACTIVATION_OPERATION,
44                true,
45            )?,
46        })
47    }
48}
49
50impl MlxBindingsContext {
51    /// Applies the dual-output rate-activation graph, returning the update
52    /// rates and the decay factors one replay produced.
53    pub fn apply_compiled_gated_delta_rate_activation(
54        &self,
55        rate_activation: &MlxCompiledGatedDeltaRateActivation,
56        update_logits: &MlxArray,
57        decay_interval_inputs: &MlxArray,
58        decay_rate_logarithm: &MlxArray,
59        decay_interval_bias: &MlxArray,
60    ) -> Result<(MlxArray, MlxArray), MlxCError> {
61        let mut graph_outputs = rate_activation.compiled_graph.apply_multi(
62            &[
63                update_logits,
64                decay_interval_inputs,
65                decay_rate_logarithm,
66                decay_interval_bias,
67            ],
68            APPLY_GATED_DELTA_RATE_ACTIVATION_OPERATION,
69        )?;
70        if graph_outputs.len() != 2 {
71            return Err(MlxCError {
72                operation: APPLY_GATED_DELTA_RATE_ACTIVATION_OPERATION,
73                description: format!(
74                    "compiled MLX gated-delta rate activation returned {} outputs, expected 2",
75                    graph_outputs.len()
76                ),
77            });
78        }
79        let decay_rates = graph_outputs
80            .pop()
81            .expect("the length check proved two outputs");
82        let update_rates = graph_outputs
83            .pop()
84            .expect("the length check proved two outputs");
85        Ok((update_rates, decay_rates))
86    }
87}
88
89unsafe extern "C" fn build_gated_delta_rate_activation_graph(
90    output_vector: *mut raw::mlx_vector_array,
91    input_vector: raw::mlx_vector_array,
92) -> c_int {
93    if output_vector.is_null() || unsafe { raw::mlx_vector_array_size(input_vector) } != 4 {
94        return 1;
95    }
96    let update_logits = match array_from_vector(input_vector, 0) {
97        Ok(update_logits) => update_logits,
98        Err(get_status) => return get_status,
99    };
100    let decay_interval_inputs = match array_from_vector(input_vector, 1) {
101        Ok(decay_interval_inputs) => decay_interval_inputs,
102        Err(get_status) => return get_status,
103    };
104    let decay_rate_logarithm = match array_from_vector(input_vector, 2) {
105        Ok(decay_rate_logarithm) => decay_rate_logarithm,
106        Err(get_status) => return get_status,
107    };
108    let decay_interval_bias = match array_from_vector(input_vector, 3) {
109        Ok(decay_interval_bias) => decay_interval_bias,
110        Err(get_status) => return get_status,
111    };
112    let gpu_stream = match MlxStream::default_gpu() {
113        Ok(gpu_stream) => gpu_stream,
114        Err(_) => return 1,
115    };
116    let update_rates = match graph_output_array(|output_array| {
117        // SAFETY: The input and stream are live, and the output is uniquely writable.
118        unsafe { raw::mlx_sigmoid(output_array, update_logits.raw(), gpu_stream.raw()) }
119    }) {
120        Ok(update_rates) => update_rates,
121        Err(build_status) => return build_status,
122    };
123    // The decay chain below is the single-output gated-delta decay graph
124    // verbatim: add bias, stable softplus through logaddexp(x, 0), the
125    // float32 exponential of the per-head decay logarithm, and the closing
126    // negate-and-exp product that keeps decay factors inside (0, 1).
127    let biased_decay_intervals = match graph_output_array(|output_array| {
128        // SAFETY: Inputs and stream are live, and the output is uniquely writable.
129        unsafe {
130            raw::mlx_add(
131                output_array,
132                decay_interval_inputs.raw(),
133                decay_interval_bias.raw(),
134                gpu_stream.raw(),
135            )
136        }
137    }) {
138        Ok(biased_decay_intervals) => biased_decay_intervals,
139        Err(build_status) => return build_status,
140    };
141    let biased_dtype = biased_decay_intervals.dtype();
142    let scalar_shape_storage = [1_i32];
143    let zero_decay_interval_scalar = match graph_output_array(|output_array| {
144        // SAFETY: The shape pointer references live storage but the zero rank
145        // means MLX reads no dimensions. Dtype and stream are valid, and the
146        // output is uniquely writable. Broadcasting this scalar keeps the
147        // compiled graph shapeless across variable token counts.
148        unsafe {
149            raw::mlx_zeros(
150                output_array,
151                scalar_shape_storage.as_ptr(),
152                0,
153                biased_dtype.to_raw(),
154                gpu_stream.raw(),
155            )
156        }
157    }) {
158        Ok(zero_decay_interval_scalar) => zero_decay_interval_scalar,
159        Err(build_status) => return build_status,
160    };
161    let decay_intervals = match graph_output_array(|output_array| {
162        // SAFETY: Inputs and stream are live, and the output is uniquely writable.
163        unsafe {
164            raw::mlx_logaddexp(
165                output_array,
166                biased_decay_intervals.raw(),
167                zero_decay_interval_scalar.raw(),
168                gpu_stream.raw(),
169            )
170        }
171    }) {
172        Ok(decay_intervals) => decay_intervals,
173        Err(build_status) => return build_status,
174    };
175    let float32_decay_logs = match graph_output_array(|output_array| {
176        // SAFETY: The input and stream are live, and the output is uniquely writable.
177        unsafe {
178            raw::mlx_astype(
179                output_array,
180                decay_rate_logarithm.raw(),
181                raw::mlx_dtype__MLX_FLOAT32,
182                gpu_stream.raw(),
183            )
184        }
185    }) {
186        Ok(float32_decay_logs) => float32_decay_logs,
187        Err(build_status) => return build_status,
188    };
189    let decay_rates = match graph_output_array(|output_array| {
190        // SAFETY: The input and stream are live, and the output is uniquely writable.
191        unsafe { raw::mlx_exp(output_array, float32_decay_logs.raw(), gpu_stream.raw()) }
192    }) {
193        Ok(decay_rates) => decay_rates,
194        Err(build_status) => return build_status,
195    };
196    let decay_products = match graph_output_array(|output_array| {
197        // SAFETY: Inputs and stream are live, and the output is uniquely writable.
198        unsafe {
199            raw::mlx_multiply(
200                output_array,
201                decay_rates.raw(),
202                decay_intervals.raw(),
203                gpu_stream.raw(),
204            )
205        }
206    }) {
207        Ok(decay_products) => decay_products,
208        Err(build_status) => return build_status,
209    };
210    let negative_decay_products = match graph_output_array(|output_array| {
211        // SAFETY: The input and stream are live, and the output is uniquely writable.
212        unsafe { raw::mlx_negative(output_array, decay_products.raw(), gpu_stream.raw()) }
213    }) {
214        Ok(negative_decay_products) => negative_decay_products,
215        Err(build_status) => return build_status,
216    };
217    let decays = match graph_output_array(|output_array| {
218        // SAFETY: The input and stream are live, and the output is uniquely writable.
219        unsafe {
220            raw::mlx_exp(
221                output_array,
222                negative_decay_products.raw(),
223                gpu_stream.raw(),
224            )
225        }
226    }) {
227        Ok(decays) => decays,
228        Err(build_status) => return build_status,
229    };
230    // Output order is the caller's ABI: update rates first, decay factors
231    // second. Multi-output tracing only supports publishing the complete
232    // result vector once at the end of the builder.
233    // SAFETY: The output vector is unique and live for this callback.
234    unsafe { set_graph_output_vector(output_vector, &[&update_rates, &decays]) }
235}