Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 23 additions & 0 deletions docs/src/dev/custom_metal_kernels.rst
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,29 @@ dimension should be less than or equal to the corresponding grid dimension.
Passing ``verbose=True`` to :func:`ast.metal_kernel.__call__` will print the
generated code for debugging purposes.

Math Mode
---------

By default :func:`fast.metal_kernel` compiles kernels with
``compile_options={"math_mode": "safe"}`` so special values follow IEEE
behavior, for example ``exp(-inf) == 0``. This is important for kernels such as
masked softmax where causal or sliding-window masks depend on exponentiating
``-inf``.

If your kernel does not rely on these edge cases, you can opt in to less strict
math with ``compile_options={"math_mode": "relaxed"}`` or
``compile_options={"math_mode": "fast"}``:

.. code-block:: python

kernel = mx.fast.metal_kernel(
name="my_kernel",
input_names=["x"],
output_names=["y"],
source=source,
compile_options={"math_mode": "relaxed"},
)

Using Shape/Strides
-------------------

Expand Down
6 changes: 4 additions & 2 deletions mlx/backend/common/metal_kernel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,8 @@ CustomKernelFunction metal_kernel(
const std::string& source,
const std::string& header /* = "" */,
bool ensure_row_contiguous /* = true */,
bool atomic_outputs /* = false */) {
bool atomic_outputs /* = false */,
const CompileOptions& compile_options /* = {} */) {
if (output_names.empty()) {
throw std::invalid_argument(
"[metal_kernel] Must specify at least one output.");
Expand Down Expand Up @@ -360,7 +361,8 @@ CustomKernelFunction metal_kernel(
init_value,
std::vector<ScalarArg>{},
false,
0),
0,
compile_options.serialize()),
std::move(inputs));
};
}
Expand Down
27 changes: 27 additions & 0 deletions mlx/backend/common/metal_kernel.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright © 2026 Apple Inc.

#pragma once

namespace mlx::core {

enum class MathMode {
Safe = 0,
Relaxed = 1,
Fast = 2,
};

struct CompileOptions {
MathMode math_mode = MathMode::Safe;

CompileOptions() = default;
bool operator==(const CompileOptions&) const = default;

// A simple way to make export work, needs more work when adding new options.
using Data = int;
CompileOptions(Data data) : math_mode(static_cast<MathMode>(data)) {}
Data serialize() const {
return static_cast<int>(math_mode);
}
};

} // namespace mlx::core
15 changes: 10 additions & 5 deletions mlx/backend/metal/custom_kernel.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright © 2024 Apple Inc.

#include "mlx/backend/common/metal_kernel.h"
#include "mlx/backend/gpu/copy.h"
#include "mlx/backend/metal/jit/includes.h"
#include "mlx/backend/metal/utils.h"
Expand All @@ -8,7 +9,8 @@
namespace mlx::core::fast {

struct CustomKernelCache {
std::unordered_map<std::string, std::string> libraries;
std::unordered_map<std::string, std::pair<std::string, CompileOptions::Data>>
libraries;
};

static CustomKernelCache& cache() {
Expand Down Expand Up @@ -58,17 +60,20 @@ void CustomKernel::eval_gpu(
auto& kernel_cache = cache();
if (auto it = kernel_cache.libraries.find(name_);
it != kernel_cache.libraries.end()) {
if (it->second != source_) {
if (it->second.first != source_ ||
it->second.second != compile_options_) {
auto& d = metal::device(s.device);
d.clear_library(name_);
it->second = source_;
it->second = std::make_tuple(source_, compile_options_);
}
} else {
kernel_cache.libraries.emplace(name_, source_);
kernel_cache.libraries.emplace(
name_, std::make_tuple(source_, compile_options_));
}
}

auto lib = d.get_library(name_, [this] { return metal::utils() + source_; });
auto lib = d.get_library(
name_, compile_options_, [this] { return metal::utils() + source_; });
auto kernel = d.get_kernel(name_, lib);
auto& compute_encoder = metal::get_command_encoder(s);
compute_encoder.set_compute_pipeline_state(kernel);
Expand Down
36 changes: 33 additions & 3 deletions mlx/backend/metal/device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,34 @@ namespace {

constexpr const char* default_mtllib_path = METAL_PATH;

void set_compile_options(
MTL::CompileOptions* mtl_options,
const CompileOptions& compile_options) {
if (__builtin_available(macOS 15, iOS 18, tvOS 18, visionOS 2, *)) {
switch (compile_options.math_mode) {
case MathMode::Safe:
mtl_options->setMathMode(MTL::MathModeSafe);
break;
case MathMode::Relaxed:
mtl_options->setMathMode(MTL::MathModeRelaxed);
break;
case MathMode::Fast:
mtl_options->setMathMode(MTL::MathModeFast);
break;
default:
throw std::invalid_argument("[metal::Device] Invalid math mode.");
}
} else {
if (compile_options.math_mode == MathMode::Relaxed) {
throw std::runtime_error(
"[metal::Device] Metal math mode `relaxed` requires macOS 15, "
"iOS 18, tvOS 18, or visionOS 2.");
}
mtl_options->setFastMathEnabled(
compile_options.math_mode == MathMode::Fast);
}
}

auto get_metal_version() {
auto get_metal_version_ = []() {
if (__builtin_available(macOS 26, iOS 26, tvOS 26, visionOS 26, *)) {
Expand Down Expand Up @@ -620,15 +648,16 @@ MTL::Library* Device::get_library(
}

NS::SharedPtr<MTL::Library> Device::build_library_(
const std::string& source_string) {
const std::string& source_string,
const CompileOptions& compile_options) {
auto pool = new_scoped_memory_pool();

auto ns_code =
NS::String::string(source_string.c_str(), NS::ASCIIStringEncoding);

NS::Error* error = nullptr;
auto options = MTL::CompileOptions::alloc()->init()->autorelease();
options->setFastMathEnabled(false);
set_compile_options(options, compile_options);
options->setLanguageVersion(get_metal_version());
#ifndef NDEBUG
if (options->languageVersion() >= MTL::LanguageVersion3_2) {
Expand Down Expand Up @@ -769,6 +798,7 @@ NS::SharedPtr<MTL::ComputePipelineState> Device::get_kernel_(

MTL::Library* Device::get_library(
const std::string& name,
const CompileOptions& compile_options,
const std::function<std::string(void)>& builder) {
{
std::shared_lock rlock(library_mtx_);
Expand All @@ -782,7 +812,7 @@ MTL::Library* Device::get_library(
return it->second.get();
}

auto mtl_lib = build_library_(builder());
auto mtl_lib = build_library_(builder(), compile_options);
library_map_.insert({name, mtl_lib});
return mtl_lib.get();
}
Expand Down
12 changes: 11 additions & 1 deletion mlx/backend/metal/device.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <unordered_set>

#include "mlx/array.h"
#include "mlx/backend/common/metal_kernel.h"
#include "mlx/backend/metal/resident.h"
#include "mlx/device.h"

Expand Down Expand Up @@ -168,8 +169,15 @@ class MLX_API Device {

MTL::Library* get_library(
const std::string& name,
const CompileOptions& compile_options,
const std::function<std::string(void)>& builder);

MTL::Library* get_library(
const std::string& name,
const std::function<std::string(void)>& builder) {
return get_library(name, {}, builder);
}

void clear_library(const std::string& name);

MTL::ComputePipelineState* get_kernel(
Expand All @@ -190,7 +198,9 @@ class MLX_API Device {
}

private:
NS::SharedPtr<MTL::Library> build_library_(const std::string& source_string);
NS::SharedPtr<MTL::Library> build_library_(
const std::string& source_string,
const CompileOptions& compile_options = {});

NS::SharedPtr<MTL::Function> get_function_(
const std::string& name,
Expand Down
4 changes: 3 additions & 1 deletion mlx/fast.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <variant>

#include "mlx/api.h"
#include "mlx/backend/common/metal_kernel.h"
#include "mlx/utils.h"

namespace mlx::core::fast {
Expand Down Expand Up @@ -75,7 +76,8 @@ MLX_API CustomKernelFunction metal_kernel(
const std::string& source,
const std::string& header = "",
bool ensure_row_contiguous = true,
bool atomic_outputs = false);
bool atomic_outputs = false,
const CompileOptions& compile_options = {});

MLX_API CustomKernelFunction cuda_kernel(
const std::string& name,
Expand Down
11 changes: 8 additions & 3 deletions mlx/fast_primitives.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <optional>
#include <variant>

#include "mlx/backend/common/metal_kernel.h"
#include "mlx/primitives.h"

namespace mlx::core::fast {
Expand Down Expand Up @@ -375,7 +376,8 @@ class CustomKernel : public Primitive {
std::optional<float> init_value,
std::vector<ScalarArg> scalar_arguments,
bool is_precompiled,
int shared_memory)
int shared_memory,
CompileOptions::Data compile_options = {})
: Primitive(stream),
name_(std::move(name)),
source_(std::move(source)),
Expand All @@ -386,7 +388,8 @@ class CustomKernel : public Primitive {
init_value_(init_value),
scalar_arguments_(std::move(scalar_arguments)),
is_precompiled_(is_precompiled),
shared_memory_(shared_memory) {}
shared_memory_(shared_memory),
compile_options_(compile_options) {}

void eval_cpu(const std::vector<array>& inputs, std::vector<array>& outputs)
override {
Expand All @@ -408,7 +411,8 @@ class CustomKernel : public Primitive {
init_value_,
scalar_arguments_,
is_precompiled_,
shared_memory_);
shared_memory_,
compile_options_);
}

private:
Expand All @@ -422,6 +426,7 @@ class CustomKernel : public Primitive {
std::vector<ScalarArg> scalar_arguments_;
bool is_precompiled_;
int shared_memory_;
CompileOptions::Data compile_options_;
};

} // namespace mlx::core::fast
50 changes: 48 additions & 2 deletions python/src/fast.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,43 @@ struct PyCustomKernelFunction {
const char* tag_;
};

mx::MathMode parse_metal_math_mode(const std::string& math_mode) {
if (math_mode == "safe") {
return mx::MathMode::Safe;
} else if (math_mode == "relaxed") {
return mx::MathMode::Relaxed;
} else if (math_mode == "fast") {
return mx::MathMode::Fast;
}
throw std::invalid_argument(
"[metal_kernel] Expected math_mode to be 'safe', 'relaxed', or 'fast'.");
}

mx::CompileOptions parse_compile_options(const nb::object& obj) {
mx::CompileOptions result;
if (obj.is_none()) {
return result;
}

if (!nb::isinstance<nb::dict>(obj)) {
throw std::invalid_argument(
"[metal_kernel] Expected `compile_options` to be a dict.");
}

nb::dict dict = nb::cast<nb::dict>(obj);
for (auto [key, value] : dict) {
auto key_str = nb::cast<std::string>(key);
if (key_str == "math_mode") {
result.math_mode = parse_metal_math_mode(nb::cast<std::string>(value));
} else {
std::ostringstream msg;
msg << "[metal_kernel] Unknown compile option `" << key_str << "`.";
throw std::invalid_argument(msg.str());
}
}
return result;
}

} // namespace

void init_fast(nb::module_& parent_module) {
Expand Down Expand Up @@ -304,15 +341,17 @@ void init_fast(nb::module_& parent_module) {
const std::string& source,
const std::string& header,
bool ensure_row_contiguous,
bool atomic_outputs) {
bool atomic_outputs,
const nb::object& compile_options) {
auto kernel = mx::fast::metal_kernel(
name,
input_names,
output_names,
source,
header,
ensure_row_contiguous,
atomic_outputs);
atomic_outputs,
parse_compile_options(compile_options));
return nb::cpp_function(
PyCustomKernelFunction(std::move(kernel), "[metal_kernel]"),
nb::kw_only(),
Expand Down Expand Up @@ -356,6 +395,7 @@ void init_fast(nb::module_& parent_module) {
"header"_a = "",
"ensure_row_contiguous"_a = true,
"atomic_outputs"_a = false,
"compile_options"_a = nb::none(),
R"pbdoc(
A jit-compiled custom Metal kernel defined from a source string.

Expand All @@ -376,6 +416,12 @@ void init_fast(nb::module_& parent_module) {
before the kernel runs. Default: ``True``.
atomic_outputs (bool): Whether to use atomic outputs in the function signature
e.g. ``device atomic<float>``. Default: ``False``.
compile_options (dict, optional): Options to compile the Metal kernel
with. Supported options:

* ``"math_mode"``: The Metal math mode: ``"safe"``, ``"relaxed"``,
or ``"fast"``. ``"safe"`` preserves IEEE behavior for special
values such as ``exp(-inf) == 0``. Default: ``"safe"``.

Returns:
Callable ``metal_kernel``.
Expand Down
Loading