Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
dc13325
add basic skeleton for gated delta update forward
tpegolotti Jun 10, 2026
6f35421
add skeleton for metal kernel - compile and run ok
tpegolotti Jun 10, 2026
8e51397
copied implementation over from mlx_lm as baseline
tpegolotti Jun 11, 2026
3b81c8b
make shapes consistent with reference
tpegolotti Jun 11, 2026
6a7f080
Add skeleton for chunkwise implementation
tpegolotti Jun 12, 2026
bb53c0f
base chunkwise implementation
tpegolotti Jun 16, 2026
a1835d8
Delete local_files directory
tpegolotti Jun 16, 2026
747182c
simdgroup matrices work for C=8
tpegolotti Jun 18, 2026
6f0ceea
ran pre-commit
tpegolotti Jun 18, 2026
9f11eab
full simdgroup main loop
tpegolotti Jun 19, 2026
7933e8a
fused wy into gated computation
tpegolotti Jun 22, 2026
bd05b92
add elementwise macros
tpegolotti Jun 23, 2026
73d10aa
add gated delta benchmark
tpegolotti Jun 24, 2026
a2e0c60
fix name
tpegolotti Jun 24, 2026
7237966
fix default C value
tpegolotti Jun 24, 2026
aa44a66
update bench script
tpegolotti Jun 24, 2026
8635620
Remove old chunkwise implementation
tpegolotti Jun 25, 2026
05713ab
Start fallback implementation
tpegolotti Jun 25, 2026
c45cd0f
Add contiguous memory copy and padding to handle generic T to get it …
tpegolotti Jun 26, 2026
2ccb066
Update gated delta benchmarking. Added Qwen3.5 dimensions
tpegolotti Jun 26, 2026
6a159af
Added first nax version
tpegolotti Jul 9, 2026
03e3676
improve inverse computation
tpegolotti Jul 13, 2026
ed76b70
Fuse status update with output computation
tpegolotti Jul 13, 2026
6d6133e
Half matmuls in invers by fusion
tpegolotti Jul 13, 2026
c898f44
Remove KP
tpegolotti Jul 13, 2026
dd1124a
Add log decay for stability
tpegolotti Jul 14, 2026
d558dbb
Make ensure row contiguous in eval_gpu
tpegolotti Jul 14, 2026
b712252
Add fallback
tpegolotti Jul 14, 2026
fb25ec5
Reverted invert
tpegolotti Jul 29, 2026
2c49bfd
Change threadgroup grid
tpegolotti Jul 30, 2026
8344147
Remove one inverse
tpegolotti Jul 30, 2026
f187201
Improve inverse
tpegolotti Jul 31, 2026
5be2b48
Pre PR changes: remove C as parameter, remove explicit padding, fix t…
tpegolotti Aug 3, 2026
4a29252
remove log space for C=8 and force cleanup inline
tpegolotti Aug 4, 2026
d86cd3e
update typing
tpegolotti Aug 4, 2026
3471ad1
added back log space
tpegolotti Aug 4, 2026
ddeebd6
cleanup .metal file
tpegolotti Aug 4, 2026
d76b0e9
Add clamping back
tpegolotti Aug 5, 2026
5913f77
Back to Horner for NAX
tpegolotti Aug 5, 2026
486889d
Add test and benchmark
tpegolotti Aug 5, 2026
7c112e4
Fix sign
tpegolotti Aug 5, 2026
df34568
Increase nax atol
tpegolotti Aug 5, 2026
e4a6d52
Removed scaling from input
tpegolotti Aug 5, 2026
19c1cb5
Add mask support in fallback
tpegolotti Aug 5, 2026
21973cf
Fix non metal build link errors
tpegolotti Aug 6, 2026
eba6ea7
Fixing more linker errors
tpegolotti Aug 6, 2026
b2bd2f4
Changed lambda function to macro
tpegolotti Aug 6, 2026
5f5065e
adding nax files
tpegolotti Aug 6, 2026
b900098
Updating kernel getters
tpegolotti Aug 6, 2026
f87efb3
Fix format error
tpegolotti Aug 6, 2026
92906e0
add jit getter
tpegolotti Aug 6, 2026
79d9a71
adding torch check on test
tpegolotti Aug 6, 2026
df5e2da
adding torch check on test
tpegolotti Aug 6, 2026
8861d85
Fixing jit linkin error
tpegolotti Aug 6, 2026
05a89f2
add support for different head sizes in fallback
tpegolotti Aug 7, 2026
4ad8724
Remove debug macros
tpegolotti Aug 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions benchmarks/python/gated_delta_bench.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import argparse
import csv
import itertools
import os
import time
from datetime import datetime
from typing import Optional, Tuple

import mlx.core as mx
import numpy as np

RED_BOLD = "\033[1;31m"
GREEN = "\033[0;32m"
RESET = "\033[0m"


N_warmup = 8
N_iter_bench = 80
N_iter_func = 5


# similar to ./blas/bench_gemm.py
def bench(f, *args):
for _ in range(N_warmup):
f(*args)
mx.synchronize()

s = time.perf_counter_ns()
for _ in range(N_iter_bench):
f(*args)
mx.synchronize()
e = time.perf_counter_ns()
return (e - s) * 1e-9 # total seconds for N_iter_bench * N_iter_func calls


def do_kernel_bench(f, *args):
ys = []
for _ in range(N_iter_func):
out, hf = f(*args)
ys.append(out)
ys.append(hf)
mx.eval(ys)
return ys


def benchmark_shape(B, T, Hk, Hv, Dk, Dv, chunk_sizes):
mx.random.seed(42)
q = mx.random.normal(shape=(B, T, Hk, Dk))
k = mx.random.normal(shape=(B, T, Hk, Dk))
k = k / (mx.linalg.norm(k, axis=-1, keepdims=True) + 1e-6)
v = mx.random.normal(shape=(B, T, Hv, Dv))
g = mx.random.normal(shape=(B, T, Hv)) * 0.1 - 1.0
b = mx.sigmoid(mx.random.normal(shape=(B, T, Hv)))

shape_str = f"B={B} T={T} Hk={Hk} Hv={Hv} Dk={Dk} Dv={Dv}"
denom = N_iter_bench * N_iter_func

os.environ["GATED_DELTA_CHUNK"] = "0"
h0 = mx.zeros((B, Hv, Dv, Dk), dtype=mx.float32)
mx.eval(*mx.fast.gated_delta_update(q, k, v, g, b, initial_state=h0))
ms_seq = (
bench(do_kernel_bench, mx.fast.gated_delta_update, q, k, v, g, b, h0)
/ denom
* 1e3
)

speedups = []
for C in (c for c in chunk_sizes if c != 0):
try:
os.environ["GATED_DELTA_CHUNK"] = str(C)
h0 = mx.zeros((B, Hv, Dv, Dk), dtype=mx.float32)
mx.eval(*mx.fast.gated_delta_update(q, k, v, g, b, initial_state=h0))
ms_c = (
bench(do_kernel_bench, mx.fast.gated_delta_update, q, k, v, g, b, h0)
/ denom
* 1e3
)
speedups.append(ms_seq / ms_c if ms_c > 0 else float("nan"))
except Exception as ex:
print(f" chunk {C} failed: {ex}")
speedups.append(float("nan"))

return shape_str, f"{ms_seq:.3f}", speedups, ms_seq


def run_benchmark(run_full, to_csv=False, csv_path="benchmark_results.csv"):
if run_full:
Bs = [1, 4, 8, 16]
Ts = [8, 64, 256, 512, 1024, 2048, 4096]
Hks = [16]
Hvs = [32]
Dks = [128]
Dvs = [128]
else:
Bs = [1, 8, 16]
Ts = [8, 512, 1024, 2048]
Hks = [16]
Hvs = [32]
Dks = [128]
Dvs = [128]

chunk_sizes = [0, 8, 16]
non_zero_Cs = [C for C in chunk_sizes if C != 0]

headers = ["B", "T", "Hk", "Hv", "Dk", "Dv", "time_seq (ms)"] + [
f"C={C} (speedup)" for C in non_zero_Cs
]

col_widths = [6, 6, 6, 6, 6, 6, 15] + [25] * (len(non_zero_Cs))
fmt = "".join(f"{{:<{w}}}" for w in col_widths)

rows = []

print(fmt.format(*headers))
print("-" * (sum(col_widths)))

for B, T, Hk, Hv, Dk, Dv in itertools.product(Bs, Ts, Hks, Hvs, Dks, Dvs):
shapes_s, base_time_s, speedups, base_time = benchmark_shape(
B, T, Hk, Hv, Dk, Dv, chunk_sizes
)
row = [f"{B}", f"{T}", f"{Hk}", f"{Hv}", f"{Dk}", f"{Dv}", base_time_s]
for speed in speedups:
row.append(f"{(base_time / speed):<8.2f} ({speed:<5.2f}x)")

print(fmt.format(*row), end="")
print(f"{RESET}")

rows.append(row)

if to_csv:
with open(csv_path, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(headers)
writer.writerows(rows)
print(f"\nResults also written to {csv_path}")


if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Gated delta benchmark")
parser.add_argument("--full", "-f", action="store_true")
parser.add_argument("--csv", "-c", action="store_true")
parser.add_argument("--csv_out", "-co", default="benchmark_results.csv")
args = parser.parse_args()

run_benchmark(args.full, to_csv=args.csv, csv_path=args.csv_out)
14 changes: 14 additions & 0 deletions mlx/backend/cuda/primitives.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ namespace mlx::core {
throw std::runtime_error(#func " has no CUDA implementation."); \
}

bool fast::GatedDeltaUpdate::use_fallback(
const int Hk,
const int Dk,
const int Hv,
const int Dv,
const bool has_mask,
Stream s) {
return true;
}

NO_GPU_MULTI(LUF)
NO_GPU_MULTI(QRF)
NO_GPU_MULTI(SVD)
Expand All @@ -32,6 +42,10 @@ NO_GPU(Cholesky)
NO_GPU_MULTI(Eig)
NO_GPU_MULTI(Eigh)

namespace fast {
NO_GPU_MULTI(GatedDeltaUpdate)
}

namespace distributed {
NO_GPU_MULTI(Send)
NO_GPU_MULTI(Recv)
Expand Down
2 changes: 2 additions & 0 deletions mlx/backend/metal/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ if(MLX_METAL_JIT)
kernels/fp4.h)

make_jit_source(steel/attn/kernels/steel_attention_nax)
make_jit_source(gated_delta_update_nax)

else()
message(
Expand Down Expand Up @@ -135,6 +136,7 @@ target_sources(
${CMAKE_CURRENT_SOURCE_DIR}/logsumexp.cpp
${CMAKE_CURRENT_SOURCE_DIR}/matmul.cpp
${CMAKE_CURRENT_SOURCE_DIR}/scaled_dot_product_attention.cpp
${CMAKE_CURRENT_SOURCE_DIR}/gated_delta_update.cpp
${CMAKE_CURRENT_SOURCE_DIR}/metal.cpp
${CMAKE_CURRENT_SOURCE_DIR}/primitives.cpp
${CMAKE_CURRENT_SOURCE_DIR}/quantized.cpp
Expand Down
200 changes: 200 additions & 0 deletions mlx/backend/metal/gated_delta_update.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// Copyright © 2024 Apple Inc.
#include <sstream>

#include "mlx/backend/common/compiled.h"
#include "mlx/backend/gpu/copy.h"
#include "mlx/backend/metal/device.h"
#include "mlx/backend/metal/kernels.h"
#include "mlx/backend/metal/kernels/defines.h"
#include "mlx/backend/metal/utils.h"
#include "mlx/fast_primitives.h"
#include "mlx/utils.h"

namespace mlx::core::fast {

bool GatedDeltaUpdate::use_fallback(
const int Hk,
const int Dk,
const int Hv,
const int Dv,
const bool has_mask,
Stream s) {
if (s.device == Device::cpu) {
return true;
}

if (has_mask) {
return true;
}

if (Dk != 128 || Dv != 128) {
return true;
}

const bool supported_heads = (Hk == 24 && Hv == 24) ||
(Hk == 32 && Hv == 32) || (Hk == 16 && Hv == 32) ||
(Hk == 16 && Hv == 48);
if (!supported_heads) {
return true;
}

return false;

return false;
}

inline array
ensure_row_contiguous(const array& x, metal::Device& d, const Stream& s) {
if (!x.flags().row_contiguous) {
array x_copy = contiguous_copy_gpu(x, s);
metal::get_command_encoder(s).add_temporary(x_copy);
return x_copy;
} else {
return x;
}
}

void GatedDeltaUpdate::eval_gpu(
const std::vector<array>& inputs,
std::vector<array>& outputs) {
auto& s = stream();
auto& d = metal::device(s.device);

auto q = ensure_row_contiguous(inputs[0], d, s);
auto k = ensure_row_contiguous(inputs[1], d, s);
auto v = ensure_row_contiguous(inputs[2], d, s);
auto g = ensure_row_contiguous(inputs[3], d, s);
auto beta = ensure_row_contiguous(inputs[4], d, s);
auto h0 = ensure_row_contiguous(inputs[5], d, s);

auto& out = outputs[0];
auto& hf = outputs[1];

int B = q.shape(0);
int T = q.shape(1);
int Hk = q.shape(2);
int Dk = q.shape(3);
int Hv = v.shape(2);
int Dv = v.shape(3);

int C = 1;
const char* threashold_env = std::getenv("GATED_DELTA_THRESH");
int threshold = threashold_env ? std::stoi(threashold_env) : 16;
if (T > threshold) {
if (metal::is_nax_available())
C = 16;
else
C = 8;
}
const char* chunk_env = std::getenv("GATED_DELTA_CHUNK");
C = chunk_env ? std::stoi(chunk_env) : C;

if (!metal::is_nax_available())
C = std::min(C, 8); // override in case nax is not available.

std::string suffix = get_type_string(q.dtype()) + "_" + std::to_string(Dk) +
"_" + std::to_string(Dv) + "_" + std::to_string(Hk) + "_" +
std::to_string(Hv);

auto& compute_encoder = metal::get_command_encoder(s);

out.set_data(allocator::malloc(out.nbytes()));
hf.set_data(allocator::malloc(hf.nbytes()));

fill_gpu(array(0, out.dtype()), out, s);

switch (C) {
case 16: {
std::string kernel_name = "gated_delta_fused_nax_";
std::string base_name = kernel_name + suffix;

base_name += "_" + std::to_string(C);

std::string hash_name = base_name;

metal::MTLFCList func_consts = {};

auto delta_kernel =
get_gated_delta_nax_kernel(d, base_name, hash_name, func_consts);

compute_encoder.set_compute_pipeline_state(delta_kernel);
compute_encoder.set_input_array(q, 0);
compute_encoder.set_input_array(k, 1);
compute_encoder.set_input_array(v, 2);
compute_encoder.set_input_array(h0, 3); // initial state in
compute_encoder.set_input_array(g, 4);
compute_encoder.set_input_array(beta, 5);
compute_encoder.set_output_array(out, 6);
compute_encoder.set_output_array(hf, 7); // final state out
compute_encoder.set_bytes(T, 8);

auto grid = MTL::Size(32, Dv / 16, B * Hv);
auto threads = MTL::Size(32, 4, 1);
compute_encoder.dispatch_threads(grid, threads);
break;
}
case 8: {
std::string kernel_name = "gated_delta_fused_chunk_";
std::string base_name = kernel_name + suffix;

base_name += "_" + std::to_string(C);

std::string hash_name = base_name;

metal::MTLFCList func_consts = {};

auto delta_kernel =
get_gated_delta_kernel(d, base_name, hash_name, func_consts);

compute_encoder.set_compute_pipeline_state(delta_kernel);
compute_encoder.set_input_array(q, 0);
compute_encoder.set_input_array(k, 1);
compute_encoder.set_input_array(v, 2);
compute_encoder.set_input_array(h0, 3); // initial state in
compute_encoder.set_input_array(g, 4);
compute_encoder.set_input_array(beta, 5);
compute_encoder.set_output_array(out, 6);
compute_encoder.set_output_array(hf, 7); // final state out
compute_encoder.set_bytes(T, 8);

auto grid = MTL::Size(32, Dv / 8, B * Hv);
auto threads = MTL::Size(32, 4, 1);
compute_encoder.dispatch_threads(grid, threads);
break;
}
case 1:
case 0: {
std::string kernel_name = "seq_gated_delta_";
std::string base_name = kernel_name + suffix;
std::string hash_name = base_name;

metal::MTLFCList func_consts = {};

auto delta_kernel =
get_gated_delta_kernel(d, base_name, hash_name, func_consts);

compute_encoder.set_compute_pipeline_state(delta_kernel);

compute_encoder.set_input_array(q, 0);
compute_encoder.set_input_array(k, 1);
compute_encoder.set_input_array(v, 2);
compute_encoder.set_input_array(g, 3);
compute_encoder.set_input_array(beta, 4);
compute_encoder.set_input_array(h0, 5);
compute_encoder.set_bytes(T, 6);
compute_encoder.set_output_array(out, 7);
compute_encoder.set_output_array(hf, 8);

auto grid = MTL::Size(32, Dv, B * Hv);
auto threads = MTL::Size(32, 4, 1);
compute_encoder.dispatch_threads(grid, threads);
break;
}
default: {
throw std::runtime_error(
"NYI: Only sequential and chunk size 8,16 are supported");
}
}
}

} // namespace mlx::core::fast
3 changes: 3 additions & 0 deletions mlx/backend/metal/jit/includes.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,7 @@ const char* fp_quantized_nax();

const char* steel_attention_nax();

const char* gated_delta_update();
const char* gated_delta_update_nax();

} // namespace mlx::core::metal
Loading
Loading