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
27 changes: 27 additions & 0 deletions docs/src/usage/export.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,33 @@ keyword arguments when calling the imported function.
out, = imported_fun(x, z=y)


Saving Metadata
---------------

You can save metadata, such as a model configuration, alongside an exported
function. The metadata is a dictionary of string keys with string, integer, or
float values:

.. code-block:: python

def fun(x, y):
return x + y

x = mx.array(1.0)
y = mx.array(1.0)
metadata = {"description": "adds two arrays", "version": 1, "scale": 0.5}
mx.export_function("add.mlxfn", fun, x, y, metadata=metadata)

Pass ``return_metadata=True`` to read the metadata back when importing:

.. code-block:: python

imported_fun, metadata = mx.import_function("add.mlxfn", return_metadata=True)

# Prints: adds two arrays
print(metadata["description"])


Exporting Modules
-----------------

Expand Down
99 changes: 82 additions & 17 deletions mlx/export.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,55 @@ array deserialize(Reader& is) {
return array(std::move(shape), type, nullptr, std::vector<array>{});
}

enum class MetadataValueType { String = 0, Int = 1, Float = 2 };

void serialize(
Writer& os,
const std::unordered_map<std::string, MetadataValue>& metadata) {
serialize(os, static_cast<uint64_t>(metadata.size()));
for (auto& [key, value] : metadata) {
serialize(os, key);
std::visit(
[&os](auto&& v) {
using T = std::decay_t<decltype(v)>;
if constexpr (std::is_same_v<T, std::string>) {
serialize(os, MetadataValueType::String);
} else if constexpr (std::is_same_v<T, int64_t>) {
serialize(os, MetadataValueType::Int);
} else if constexpr (std::is_same_v<T, double>) {
serialize(os, MetadataValueType::Float);
}
serialize(os, v);
},
value);
}
}

template <>
std::unordered_map<std::string, MetadataValue> deserialize(Reader& is) {
std::unordered_map<std::string, MetadataValue> metadata;
auto size = deserialize<uint64_t>(is);
metadata.reserve(size);
for (uint64_t i = 0; i < size; ++i) {
auto key = deserialize<std::string>(is);
switch (deserialize<MetadataValueType>(is)) {
case MetadataValueType::String:
metadata.emplace(std::move(key), deserialize<std::string>(is));
break;
case MetadataValueType::Int:
metadata.emplace(std::move(key), deserialize<int64_t>(is));
break;
case MetadataValueType::Float:
metadata.emplace(std::move(key), deserialize<double>(is));
break;
default:
throw std::runtime_error(
"[import_function] Unknown metadata value type.");
}
}
return metadata;
}

template <typename, typename = void>
constexpr bool has_state = false;

Expand Down Expand Up @@ -521,10 +570,15 @@ struct PrimitiveFactory {
};
};

void write_header(Writer& os, int count, bool shapeless) {
void write_header(
Writer& os,
int count,
bool shapeless,
const std::unordered_map<std::string, MetadataValue>& metadata) {
serialize(os, std::string(version()));
serialize(os, count);
serialize(os, shapeless);
serialize(os, metadata);
}

// A struct to hold and retrieve the graphs that are exported / imported
Expand Down Expand Up @@ -674,14 +728,16 @@ FunctionTable::Function* FunctionTable::find(
FunctionExporter::FunctionExporter(
const std::string& file,
std::function<std::vector<array>(const Args&, const Kwargs&)> fun,
bool shapeless)
bool shapeless,
std::unordered_map<std::string, MetadataValue> metadata)
: os(file),
fun(std::move(fun)),
ftable(std::make_shared<FunctionTable>(shapeless)) {
ftable(std::make_shared<FunctionTable>(shapeless)),
metadata_(std::move(metadata)) {
if (!os.is_open()) {
throw std::runtime_error("[export_function] Failed to open " + file);
}
write_header(os, count, shapeless);
write_header(os, count, shapeless, metadata_);
}

FunctionExporter::FunctionExporter(
Expand Down Expand Up @@ -819,7 +875,7 @@ void FunctionExporter::export_function(const Args& args, const Kwargs& kwargs) {
// Update the header
auto pos = os.tell();
os.seek(0);
write_header(os, count, ftable->shapeless);
write_header(os, count, ftable->shapeless, metadata_);
os.seek(pos);
serialize(os, kwarg_keys);

Expand Down Expand Up @@ -898,53 +954,61 @@ void FunctionExporter::operator()(const Args& args, const Kwargs& kwargs) {
FunctionExporter exporter(
const std::string& file,
const std::function<std::vector<array>(const Args&)>& fun,
bool shapeless /* = false */) {
bool shapeless /* = false */,
const std::unordered_map<std::string, MetadataValue>& metadata /* = {} */) {
return FunctionExporter{
file,
[fun](const Args& args, const Kwargs&) { return fun(args); },
shapeless};
shapeless,
metadata};
}

FunctionExporter exporter(
const std::string& file,
const std::function<std::vector<array>(const Kwargs&)>& fun,
bool shapeless /* = false */) {
bool shapeless /* = false */,
const std::unordered_map<std::string, MetadataValue>& metadata /* = {} */) {
return exporter(
file,
[fun](const Args&, const Kwargs kwargs) { return fun(kwargs); },
shapeless);
shapeless,
metadata);
}

FunctionExporter exporter(
const std::string& file,
const std::function<std::vector<array>(const Args&, const Kwargs&)>& fun,
bool shapeless /* = false */) {
return FunctionExporter{file, fun, shapeless};
bool shapeless /* = false */,
const std::unordered_map<std::string, MetadataValue>& metadata /* = {} */) {
return FunctionExporter{file, fun, shapeless, metadata};
}

void export_function(
const std::string& file,
const std::function<std::vector<array>(const Args&)>& fun,
const Args& args,
bool shapeless /* = false */) {
exporter(file, fun, shapeless)(args);
bool shapeless /* = false */,
const std::unordered_map<std::string, MetadataValue>& metadata /* = {} */) {
exporter(file, fun, shapeless, metadata)(args);
}

void export_function(
const std::string& file,
const std::function<std::vector<array>(const Kwargs&)>& fun,
const Kwargs& kwargs,
bool shapeless /* = false */) {
exporter(file, fun, shapeless)(kwargs);
bool shapeless /* = false */,
const std::unordered_map<std::string, MetadataValue>& metadata /* = {} */) {
exporter(file, fun, shapeless, metadata)(kwargs);
}

void export_function(
const std::string& file,
const std::function<std::vector<array>(const Args&, const Kwargs&)>& fun,
const Args& args,
const Kwargs& kwargs,
bool shapeless /* = false */) {
exporter(file, fun, shapeless)(args, kwargs);
bool shapeless /* = false */,
const std::unordered_map<std::string, MetadataValue>& metadata /* = {} */) {
exporter(file, fun, shapeless, metadata)(args, kwargs);
}

FunctionExporter exporter(
Expand Down Expand Up @@ -1054,6 +1118,7 @@ ImportedFunction::ImportedFunction(const std::string& file)
auto mlx_version = deserialize<std::string>(is);
auto function_count = deserialize<int>(is);
ftable->shapeless = deserialize<bool>(is);
metadata_ = deserialize<std::unordered_map<std::string, MetadataValue>>(is);
std::unordered_map<std::uintptr_t, array> constants;

auto import_one = [&]() {
Expand Down
23 changes: 17 additions & 6 deletions mlx/export.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ namespace mlx::core {
using Args = std::vector<array>;
using Kwargs = std::unordered_map<std::string, array>;

// Metadata values saved alongside an exported function. The integer and
// floating point alternatives are 64-bit to hold Python int and float without
// narrowing (e.g. a parameter count above 2^31, or a non-power-of-two float).
using MetadataValue = std::variant<std::string, int64_t, double>;

// Possible types for a Primitive's state
using StateT = std::variant<
bool,
Expand Down Expand Up @@ -50,17 +55,20 @@ struct FunctionExporter;
MLX_API FunctionExporter exporter(
const std::string& file,
const std::function<std::vector<array>(const Args&)>& fun,
bool shapeless = false);
bool shapeless = false,
const std::unordered_map<std::string, MetadataValue>& metadata = {});

MLX_API FunctionExporter exporter(
const std::string& file,
const std::function<std::vector<array>(const Kwargs&)>& fun,
bool shapeless = false);
bool shapeless = false,
const std::unordered_map<std::string, MetadataValue>& metadata = {});

MLX_API FunctionExporter exporter(
const std::string& path,
const std::function<std::vector<array>(const Args&, const Kwargs&)>& fun,
bool shapeless = false);
bool shapeless = false,
const std::unordered_map<std::string, MetadataValue>& metadata = {});

/**
* Export a function to a file.
Expand All @@ -69,20 +77,23 @@ MLX_API void export_function(
const std::string& file,
const std::function<std::vector<array>(const Args&)>& fun,
const Args& args,
bool shapeless = false);
bool shapeless = false,
const std::unordered_map<std::string, MetadataValue>& metadata = {});

MLX_API void export_function(
const std::string& file,
const std::function<std::vector<array>(const Kwargs&)>& fun,
const Kwargs& kwargs,
bool shapeless = false);
bool shapeless = false,
const std::unordered_map<std::string, MetadataValue>& metadata = {});

MLX_API void export_function(
const std::string& file,
const std::function<std::vector<array>(const Args&, const Kwargs&)>& fun,
const Args& args,
const Kwargs& kwargs,
bool shapeless = false);
bool shapeless = false,
const std::unordered_map<std::string, MetadataValue>& metadata = {});

struct ImportedFunction;

Expand Down
19 changes: 15 additions & 4 deletions mlx/export_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,20 @@ struct MLX_API FunctionExporter {
friend MLX_API FunctionExporter exporter(
const std::string&,
const std::function<std::vector<array>(const Args&)>&,
bool shapeless);
bool shapeless,
const std::unordered_map<std::string, MetadataValue>& metadata);

friend MLX_API FunctionExporter exporter(
const std::string&,
const std::function<std::vector<array>(const Kwargs&)>&,
bool shapeless);
bool shapeless,
const std::unordered_map<std::string, MetadataValue>& metadata);

friend MLX_API FunctionExporter exporter(
const std::string&,
const std::function<std::vector<array>(const Args&, const Kwargs&)>&,
bool shapeless);
bool shapeless,
const std::unordered_map<std::string, MetadataValue>& metadata);

friend MLX_API FunctionExporter exporter(
const ExportCallback&,
Expand All @@ -57,7 +60,8 @@ struct MLX_API FunctionExporter {
FunctionExporter(
const std::string& file,
std::function<std::vector<array>(const Args&, const Kwargs&)> fun,
bool shapeless);
bool shapeless,
std::unordered_map<std::string, MetadataValue> metadata);

FunctionExporter(
const ExportCallback& callback,
Expand All @@ -77,6 +81,7 @@ struct MLX_API FunctionExporter {
int count{0};
bool closed{false};
std::shared_ptr<FunctionTable> ftable;
std::unordered_map<std::string, MetadataValue> metadata_;
};

struct MLX_API ImportedFunction {
Expand All @@ -88,12 +93,18 @@ struct MLX_API ImportedFunction {
std::vector<array> operator()(const Kwargs& kwargs) const;
std::vector<array> operator()(const Args& args, const Kwargs& kwargs) const;

// The metadata saved with the function when it was exported.
const std::unordered_map<std::string, MetadataValue>& metadata() const {
return metadata_;
}

private:
ImportedFunction(const std::string& file);
friend MLX_API ImportedFunction import_function(const std::string&);
ImportedFunction();

std::shared_ptr<FunctionTable> ftable;
std::unordered_map<std::string, MetadataValue> metadata_;
};

} // namespace mlx::core
Loading