# Copyright © 2025 Apple Inc. import mlx.core as mx import mlx.nn as nn from ..utils import can_run_metal def _make_kl_forward_kernel(): if not can_run_metal(): return source = """ constexpr int M = 4; constexpr int block = 1024 * M; constexpr int full_blocks = V / block; constexpr int extra = V - full_blocks * block; threadgroup float shared[32 * 2]; uint out_idx = threadgroup_position_in_grid.y; uint simd_lane_id = thread_index_in_simdgroup; uint simd_group_id = simdgroup_index_in_threadgroup; logits_q += out_idx * V; logits_p += out_idx * V; out += out_idx; float lse_q_minus_p; float lse_p; { float max_q = -1e30; float max_p = -1e30; float sum_exp_q = 0; float sum_exp_p = 0; int offset = thread_index_in_threadgroup * M; for (int i = 0; i < full_blocks; i++) { // Read and update q and p float vals_q[M]; float vals_p[M]; for (int j=0; j 0) { // Read and update q and p float vals_q[M]; float vals_p[M]; for (int j=0; j < M; j++) { vals_q[j] = (offset + j < V) ? logits_q[offset + j] : -1e30; vals_p[j] = (offset + j < V) ? logits_p[offset + j] : -1e30; } float prev_max_q = max_q; float prev_max_p = max_p; for (int j=0; j 0) { float vals_q[M]; float vals_p[M]; for (int j=0; j(kl); } } """ return mx.fast.metal_kernel( name="kl_forward", input_names=["logits_q", "logits_p"], output_names=["out"], source=source, ensure_row_contiguous=True, ) def _make_kl_backward_kernel(): if not can_run_metal(): return source = """ constexpr int M = 4; constexpr int block = 1024 * M; constexpr int full_blocks = V / block; constexpr int extra = V - full_blocks * block; threadgroup float shared[32 * 2]; uint out_idx = threadgroup_position_in_grid.y; uint simd_lane_id = thread_index_in_simdgroup; uint simd_group_id = simdgroup_index_in_threadgroup; logits_q += out_idx * V; logits_p += out_idx * V; out += out_idx * V; cotan += out_idx; float lse_q; float lse_p; { float max_q = -1e30; float max_p = -1e30; float sum_exp_q = 0; float sum_exp_p = 0; int offset = thread_index_in_threadgroup * M; for (int i = 0; i < full_blocks; i++) { // Read and update q and p float vals_q[M]; float vals_p[M]; for (int j=0; j 0) { // Read and update q and p float vals_q[M]; float vals_p[M]; for (int j=0; j < M; j++) { vals_q[j] = (offset + j < V) ? logits_q[offset + j] : -1e30; vals_p[j] = (offset + j < V) ? logits_p[offset + j] : -1e30; } float prev_max_q = max_q; float prev_max_p = max_p; for (int j=0; j( c * (metal::fast::exp(vals_q[j] - lse_q) - metal::fast::exp(vals_p[j] - lse_p))); } // Move to the next block offset += block; } if (extra > 0) { float vals_q[M]; float vals_p[M]; for (int j=0; j( c * (metal::fast::exp(vals_q[j] - lse_q) - metal::fast::exp(vals_p[j] - lse_p))); } } } } """ return mx.fast.metal_kernel( name="kl_backward", input_names=["logits_q", "logits_p", "cotan"], output_names=["out"], source=source, ensure_row_contiguous=True, ) _kl_forward_kernel = _make_kl_forward_kernel() _kl_backward_kernel = _make_kl_backward_kernel() @mx.custom_function def _kl_div_loss(logits_q, logits_p): n_outs = logits_q.size // logits_q.shape[-1] dt = logits_q.dtype return _kl_forward_kernel( inputs=[logits_q, logits_p], output_shapes=[logits_q.shape[:-1]], output_dtypes=[dt], template=[("T", dt), ("V", logits_q.shape[-1])], grid=(1024, n_outs, 1), threadgroup=(1024, 1, 1), )[0] @_kl_div_loss.vjp def _kl_div_loss(primals, cotangent, output): logits_q, logits_p = primals dt = logits_q.dtype dp = mx.zeros_like(logits_p) dq = _kl_backward_kernel( inputs=[logits_q, logits_p, cotangent], output_shapes=[logits_q.shape], output_dtypes=[dt], template=[("T", dt), ("V", logits_q.shape[-1])], grid=(1024, cotangent.size, 1), threadgroup=(1024, 1, 1), )[0] return dq, dp def kl_div_loss(logits_q, logits_p): if can_run_metal(): return _kl_div_loss(logits_q, logits_p) else: return nn.losses.kl_div_loss( logits_q - mx.logsumexp(logits_q, axis=-1, keepdims=True), logits_p - mx.logsumexp(logits_p, axis=-1, keepdims=True), axis=-1, reduction="none", ) def _make_js_forward_kernel(): if not can_run_metal(): return source = """ constexpr int M = 4; constexpr int block = 1024 * M; constexpr int full_blocks = V / block; constexpr int extra = V - full_blocks * block; threadgroup float shared[32 * 2]; uint out_idx = threadgroup_position_in_grid.y; uint simd_lane_id = thread_index_in_simdgroup; uint simd_group_id = simdgroup_index_in_threadgroup; logits_q += out_idx * V; logits_p += out_idx * V; out += out_idx; out_kl_q += out_idx; float lse_p; float lse_q; { float max_q = -1e30; float max_p = -1e30; float sum_exp_q = 0; float sum_exp_p = 0; int offset = thread_index_in_threadgroup * M; for (int i = 0; i < full_blocks; i++) { // Read and update q and p float vals_q[M]; float vals_p[M]; for (int j=0; j 0) { // Read and update q and p float vals_q[M]; float vals_p[M]; for (int j=0; j < M; j++) { vals_q[j] = (offset + j < V) ? logits_q[offset + j] : -1e30; vals_p[j] = (offset + j < V) ? logits_p[offset + j] : -1e30; } float prev_max_q = max_q; float prev_max_p = max_p; for (int j=0; j(2)); int offset = thread_index_in_threadgroup * M; for (int i = 0; i < full_blocks; i++) { // Read and add to the kl_p and kl_q float vals_q[M]; float vals_p[M]; for (int j=0; j 0) { float vals_q[M]; float vals_p[M]; for (int j=0; j(0.5 * kl_p + 0.5 * kl_q); out_kl_q[0] = static_cast(kl_q); } } """ return mx.fast.metal_kernel( name="js_forward", input_names=["logits_q", "logits_p"], output_names=["out", "out_kl_q"], source=source, ensure_row_contiguous=True, ) def _make_js_backward_kernel(): if not can_run_metal(): return source = """ constexpr int M = 4; constexpr int block = 1024 * M; constexpr int full_blocks = V / block; constexpr int extra = V - full_blocks * block; threadgroup float shared[32 * 2]; uint out_idx = threadgroup_position_in_grid.y; uint simd_lane_id = thread_index_in_simdgroup; uint simd_group_id = simdgroup_index_in_threadgroup; logits_q += out_idx * V; logits_p += out_idx * V; out_q += out_idx * V; cotan += out_idx; output_kl_q += out_idx; float lse_q; float lse_p; { float max_q = -1e30; float max_p = -1e30; float sum_exp_q = 0; float sum_exp_p = 0; int offset = thread_index_in_threadgroup * M; for (int i = 0; i < full_blocks; i++) { // Read and update q and p float vals_q[M]; float vals_p[M]; for (int j=0; j 0) { // Read and update q and p float vals_q[M]; float vals_p[M]; for (int j=0; j < M; j++) { vals_q[j] = (offset + j < V) ? logits_q[offset + j] : -1e30; vals_p[j] = (offset + j < V) ? logits_p[offset + j] : -1e30; } float prev_max_q = max_q; float prev_max_p = max_p; for (int j=0; j(2)); float kl_q = output_kl_q[0]; int offset = thread_index_in_threadgroup * M; for (int i = 0; i < full_blocks; i++) { // Read and compute vjp for logits_q float vals_q[M]; float vals_p[M]; for (int j=0; j( c * 0.5 * q_j * (logtwo - metal::fast::log(1 + metal::fast::exp(logp_j - logq_j)) - kl_q) ); } // Move to the next block offset += block; } if (extra > 0) { float vals_q[M]; float vals_p[M]; for (int j=0; j( c * 0.5 * q_j * (logtwo - metal::fast::log(1 + metal::fast::exp(logp_j - logq_j)) - kl_q) ); } } } } """ return mx.fast.metal_kernel( name="js_backward", input_names=["logits_q", "logits_p", "cotan", "output_kl_q"], output_names=["out_q"], source=source, ensure_row_contiguous=True, ) _js_forward_kernel = _make_js_forward_kernel() _js_backward_kernel = _make_js_backward_kernel() @mx.custom_function def _js_div_loss(logits_q, logits_p): n_outs = logits_q.size // logits_q.shape[-1] dt = logits_q.dtype outputs = _js_forward_kernel( inputs=[logits_q, logits_p], output_shapes=[logits_q.shape[:-1], logits_q.shape[:-1]], output_dtypes=[dt, dt], template=[("T", dt), ("V", logits_q.shape[-1])], grid=(1024, n_outs, 1), threadgroup=(1024, 1, 1), ) return outputs[0], mx.stop_gradient(outputs[1]) @_js_div_loss.vjp def _js_div_loss(primals, cotangents, outputs): logits_q, logits_p = primals cotan, _ = cotangents _, kl_q = outputs dt = logits_q.dtype dp = mx.zeros_like(logits_p) dq = _js_backward_kernel( inputs=[logits_q, logits_p, cotan, kl_q], output_shapes=[logits_q.shape], output_dtypes=[dt], template=[("T", dt), ("V", logits_q.shape[-1])], grid=(1024, cotan.size, 1), threadgroup=(1024, 1, 1), ) return dq, dp def js_div_loss(logits_q, logits_p): if can_run_metal(): return _js_div_loss(logits_q, logits_p)[0] else: logprobs_p = logits_p - mx.logsumexp(logits_p, axis=-1, keepdims=True) logprobs_q = logits_q - mx.logsumexp(logits_q, axis=-1, keepdims=True) logprobs_m = ( logprobs_p + mx.log(1 + mx.exp(logprobs_q - logprobs_p)) - mx.log(2).astype(logits_q.dtype) ) kl_p = nn.losses.kl_div_loss(logprobs_m, logprobs_p, axis=-1, reduction="none") kl_q = nn.losses.kl_div_loss(logprobs_m, logprobs_q, axis=-1, reduction="none") return 0.5 * (kl_p + kl_q)