Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
c168d5c
add basic skeleton for gated delta update forward
tpegolotti Jun 10, 2026
1f00f3c
add skeleton for metal kernel - compile and run ok
tpegolotti Jun 10, 2026
5cbd03f
copied implementation over from mlx_lm as baseline
tpegolotti Jun 11, 2026
7a81134
make shapes consistent with reference
tpegolotti Jun 11, 2026
381dbd5
Add skeleton for chunkwise implementation
tpegolotti Jun 12, 2026
8afda35
base chunkwise implementation
tpegolotti Jun 16, 2026
89ac94c
Delete local_files directory
tpegolotti Jun 16, 2026
7266c2e
simdgroup matrices work for C=8
tpegolotti Jun 18, 2026
76e3c4b
ran pre-commit
tpegolotti Jun 18, 2026
8507bf1
full simdgroup main loop
tpegolotti Jun 19, 2026
568338a
fused wy into gated computation
tpegolotti Jun 22, 2026
77f62ff
add elementwise macros
tpegolotti Jun 23, 2026
5efb380
add gated delta benchmark
tpegolotti Jun 24, 2026
5aac26e
fix name
tpegolotti Jun 24, 2026
ef6a3e8
fix default C value
tpegolotti Jun 24, 2026
a41fdec
update bench script
tpegolotti Jun 24, 2026
be961cd
Remove old chunkwise implementation
tpegolotti Jun 25, 2026
0251091
Start fallback implementation
tpegolotti Jun 25, 2026
fc9f3a7
Add contiguous memory copy and padding to handle generic T to get it …
tpegolotti Jun 26, 2026
22e285f
Update gated delta benchmarking. Added Qwen3.5 dimensions
tpegolotti Jun 26, 2026
53c3a50
Added first nax version
tpegolotti Jul 9, 2026
65dc671
improve inverse computation
tpegolotti Jul 13, 2026
7c1edbe
Fuse status update with output computation
tpegolotti Jul 13, 2026
1b54e78
Half matmuls in invers by fusion
tpegolotti Jul 13, 2026
b688a77
Remove KP
tpegolotti Jul 13, 2026
1a05e31
Add log decay for stability
tpegolotti Jul 14, 2026
bc82ef6
Make ensure row contiguous in eval_gpu
tpegolotti Jul 14, 2026
2b8f7f3
Add fallback
tpegolotti Jul 14, 2026
fa5eef0
Reverted invert
tpegolotti Jul 29, 2026
84ad09e
Change threadgroup grid
tpegolotti Jul 30, 2026
ee185f5
Remove one inverse
tpegolotti Jul 30, 2026
f465b66
Improve inverse
tpegolotti Jul 31, 2026
3461841
Pre PR changes: remove C as parameter, remove explicit padding, fix t…
tpegolotti Aug 3, 2026
c34f133
remove log space for C=8 and force cleanup inline
tpegolotti Aug 4, 2026
69f7df7
update typing
tpegolotti Aug 4, 2026
be74c6e
added back log space
tpegolotti Aug 4, 2026
1354488
cleanup .metal file
tpegolotti Aug 4, 2026
0c9db8b
Add clamping back
tpegolotti Aug 5, 2026
11fe0d8
Back to Horner for NAX
tpegolotti Aug 5, 2026
7eece10
Add test and benchmark
tpegolotti Aug 5, 2026
c477ff2
Fix sign
tpegolotti Aug 5, 2026
8635a64
Increase nax atol
tpegolotti Aug 5, 2026
a81605c
Removed scaling from input
tpegolotti Aug 5, 2026
aa8362b
Add mask support in fallback
tpegolotti Aug 5, 2026
4989ca3
Fix non metal build link errors
tpegolotti Aug 6, 2026
b08132a
Fixing more linker errors
tpegolotti Aug 6, 2026
87482d0
Changed lambda function to macro
tpegolotti Aug 6, 2026
9553ccc
adding nax files
tpegolotti Aug 6, 2026
9dd0fe0
Updating kernel getters
tpegolotti Aug 6, 2026
b736b1a
Fix format error
tpegolotti Aug 6, 2026
fb245b6
add jit getter
tpegolotti Aug 6, 2026
348d12f
adding torch check on test
tpegolotti Aug 6, 2026
c610b6c
adding torch check on test
tpegolotti Aug 6, 2026
ce30875
Fixing jit linkin error
tpegolotti Aug 6, 2026
3309997
add support for different head sizes in fallback
tpegolotti Aug 7, 2026
1693325
Remove debug macros
tpegolotti Aug 7, 2026
53655e6
Fallback output dtype corresponds to input dtype
tpegolotti Aug 25, 2026
2f6f47f
Addressing PR comments
tpegolotti Aug 25, 2026
2303711
Add force_fused option to scaled_dot_product_attention (#4185)
DT-HJ Aug 18, 2026
b7e9ed2
Add a fused full-attention path for head_dim 256 on NAX devices (#3842)
wyanzhao Aug 19, 2026
7b54f80
use fallback now considers new head dimensions
tpegolotti Aug 26, 2026
552d54e
Run formatter
tpegolotti Aug 26, 2026
5ae919d
Addressing comments
tpegolotti Aug 27, 2026
1b49f19
Removed tensor ops header
tpegolotti Sep 14, 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,13 +24,27 @@ 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)
NO_GPU(Inverse)
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 @@ -102,6 +102,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 @@ -136,6 +137,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
167 changes: 167 additions & 0 deletions mlx/backend/metal/gated_delta_update.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// 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) || (Hk == 16 && Hv == 16) ||
(Hk == 16 && Hv == 64);
if (!supported_heads) {
return true;
}

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;
int threshold = env::get_var("GATED_DELTA_THRESH", 16);
if (T >= threshold)
C = metal::is_nax_available() ? 16 : 8;

C = env::get_var("GATED_DELTA_CHUNK", C);

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

std::string suffix;
concatenate(
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()));

switch (C) {
case 16: {
std::string kernel_name = "gated_delta_fused_nax_";
std::string base_name;
concatenate(base_name, kernel_name, suffix, "_16");
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);
break;
}
case 8: {
std::string kernel_name = "gated_delta_fused_chunk_";
std::string base_name;
concatenate(base_name, kernel_name, suffix, "_8");
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);
break;
}
case 0:
C = 1; // avoiding a div by zero on the grid.
case 1: {
std::string kernel_name = "seq_gated_delta_";
std::string base_name;
concatenate(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);
break;
}
default: {
throw std::runtime_error(
"NYI: Only sequential (C=1) and chunk size 8,16 are supported");
}
}
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 / C, B * Hv);
auto threads = MTL::Size(32, 4, 1);
compute_encoder.dispatch_threads(grid, threads);
}

} // 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 @@ -59,4 +59,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