Skip to content
Merged
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
9 changes: 5 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ MLX_STAGE_DIR := $(PRIV_DIR)/mlx/lib

BUILD_DIR := $(EMILY_CACHE_DIR)/build-$(EMILY_VERSION)

# Sources
SOURCES := $(wildcard c_src/*.cpp)
HEADERS := $(wildcard c_src/*.h) $(wildcard c_src/*.hpp)
# Sources — include ops/* and any other subdirs under c_src.
SOURCES := $(shell find c_src -name '*.cpp')
HEADERS := $(shell find c_src \( -name '*.h' -o -name '*.hpp' \))
OBJECTS := $(patsubst c_src/%.cpp,$(BUILD_DIR)/%.o,$(SOURCES))

# Flags
CXXFLAGS := -std=c++17 -O3 -fPIC -fvisibility=hidden -Wall -Wextra
CXXFLAGS += -I$(ERTS_INCLUDE_DIR)
CXXFLAGS += -I$(ERTS_INCLUDE_DIR) -Ic_src
# Third-party headers: use -isystem so warnings inside them (e.g. MLX's
# -Wdeprecated-copy on _MLX_BFloat16) don't clutter our builds or trip
# -Werror.
Expand Down Expand Up @@ -41,6 +41,7 @@ $(PRIV_DIR):
@mkdir -p $(PRIV_DIR)

$(BUILD_DIR)/%.o: c_src/%.cpp $(HEADERS) | $(BUILD_DIR)
@mkdir -p $(dir $@)
$(CXX) $(CXXFLAGS) -c $< -o $@

$(MLX_STAGE_DIR): | $(PRIV_DIR)
Expand Down
42 changes: 42 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,45 @@
- M0 scaffold: mix project, MLX 0.25.1 prebuilt fetch pipeline,
Makefile wiring `fine` + MLX, `Emily.Native` NIF surface for tensor
round-trip, application supervisor skeleton, smoke test suite.
- M1 (partial) — `Emily.Native` op inventory. Shared headers in
`c_src/emily/` (dtype mapping, Tensor resource, helpers);
per-category op files under `c_src/ops/`:
- Creation: `zeros`, `ones`, `full`, `arange`, `eye`.
- Cast: `astype`.
- Unary elementwise: `negative`, `abs`, `sign`, `floor`, `ceil`,
`sqrt`, `rsqrt`, `exp`, `expm1`, `log`, `log1p`, `log2`, `log10`,
trig/inverse-trig/hyperbolic family, `sigmoid`, `erf`, `erfinv`,
`square`, `reciprocal`, `logical_not`, `bitwise_invert`, `isnan`,
`isinf`, `isfinite`, `conjugate`, `real`, `imag`, `stop_gradient`,
`round` (with decimals).
- Binary elementwise: `add`, `subtract`, `multiply`, `divide`,
`floor_divide`, `remainder`, `power`, `maximum`, `minimum`,
`logaddexp`, `arctan2`.
- Compare: `equal`, `not_equal`, `less`, `less_equal`, `greater`,
`greater_equal`.
- Logical: `logical_and`, `logical_or`.
- Bitwise: `bitwise_and`, `bitwise_or`, `bitwise_xor`, `left_shift`,
`right_shift`.
- Reductions (axes + keepdims): `sum`, `mean`, `prod`, `max`, `min`,
`all`, `any`, `logsumexp`; plus `var`/`std` with `ddof`,
`argmax`/`argmin`, cumulative `cumsum`/`cumprod`/`cummax`/`cummin`.
- Shape: `reshape`, `transpose`, `squeeze`, `expand_dims`,
`broadcast_to`, `concatenate`, `stack`, `flatten`, `tile`,
`swapaxes`, `pad`, `repeat`.
- Indexing: `slice`, `take`, `where`.
- Linalg: `matmul`, `tensordot`, `outer`, `inner`.
- `Emily.Native.to_binary/1` now routes through `mx::contiguous` so
strided views (transpose/slice/swapaxes/broadcast) materialize
correctly.
- Makefile compiles `c_src/**/*.cpp` recursively.

## Notes

- Ops files use anonymous namespaces to prevent NIF function names
(`sin`, `log1p`, `sqrt`, ...) from colliding with C math-library
symbols pulled in by MLX headers.
- Deferred to later iterations of M1: sort/argsort, clip,
`slice_update`, `take_along_axis`, `scatter*`, convolutions,
`hadamard_transform`, random ops, FFT, quantized ops,
memory-stats/soak tests. Tracked for M1 completion before moving to
M2 (Backend).
61 changes: 61 additions & 0 deletions c_src/emily/dtype.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Dtype translation between Nx's {kind, bits} tuples and mlx::Dtype.
//
// Nx kinds we honour: "f" (float), "bf" (bfloat), "s" (signed int),
// "u" (unsigned int), "c" (complex), "pred" (1-bit bool, MLX stores
// as one byte).

#pragma once

#include <fine.hpp>
#include <mlx/mlx.h>

#include <cstdint>
#include <stdexcept>
#include <string>
#include <tuple>

namespace emily {

namespace mx = mlx::core;

inline mx::Dtype to_mlx_dtype(const std::string &kind, int64_t bits) {
if (kind == "f" && bits == 32) return mx::float32;
if (kind == "f" && bits == 16) return mx::float16;
if (kind == "bf" && bits == 16) return mx::bfloat16;
if (kind == "s" && bits == 8) return mx::int8;
if (kind == "s" && bits == 16) return mx::int16;
if (kind == "s" && bits == 32) return mx::int32;
if (kind == "s" && bits == 64) return mx::int64;
if (kind == "u" && bits == 8) return mx::uint8;
if (kind == "u" && bits == 16) return mx::uint16;
if (kind == "u" && bits == 32) return mx::uint32;
if (kind == "u" && bits == 64) return mx::uint64;
if (kind == "c" && bits == 64) return mx::complex64;
if (kind == "pred") return mx::bool_;

throw std::invalid_argument(
"unsupported dtype: {" + kind + ", " + std::to_string(bits) + "}");
}

inline mx::Dtype to_mlx_dtype(const std::tuple<fine::Atom, int64_t> &t) {
return to_mlx_dtype(std::get<0>(t).to_string(), std::get<1>(t));
}

inline std::tuple<fine::Atom, int64_t> from_mlx_dtype(mx::Dtype dtype) {
if (dtype == mx::float32) return {fine::Atom("f"), 32};
if (dtype == mx::float16) return {fine::Atom("f"), 16};
if (dtype == mx::bfloat16) return {fine::Atom("bf"), 16};
if (dtype == mx::int8) return {fine::Atom("s"), 8};
if (dtype == mx::int16) return {fine::Atom("s"), 16};
if (dtype == mx::int32) return {fine::Atom("s"), 32};
if (dtype == mx::int64) return {fine::Atom("s"), 64};
if (dtype == mx::uint8) return {fine::Atom("u"), 8};
if (dtype == mx::uint16) return {fine::Atom("u"), 16};
if (dtype == mx::uint32) return {fine::Atom("u"), 32};
if (dtype == mx::uint64) return {fine::Atom("u"), 64};
if (dtype == mx::complex64) return {fine::Atom("c"), 64};
if (dtype == mx::bool_) return {fine::Atom("pred"), 1};
throw std::runtime_error("unmapped mlx dtype");
}

} // namespace emily
62 changes: 62 additions & 0 deletions c_src/emily/tensor.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Tensor: opaque resource wrapping mlx::core::array.
//
// MLX arrays are refcounted internally; our ResourcePtr<Tensor> adds
// one BEAM-managed ref. No manual atomics, no custom destructor — fine
// and MLX together do the right thing.
//
// Helpers: wrap/unwrap shortcuts + shape conversion between Nx's
// list-of-int64 format and MLX's std::vector<int32_t> Shape.

#pragma once

#include "dtype.hpp"

#include <fine.hpp>
#include <mlx/mlx.h>

#include <cstdint>
#include <stdexcept>
#include <string>
#include <vector>

namespace emily {

namespace mx = mlx::core;

class Tensor {
public:
Tensor(mx::array a) : array(std::move(a)) {}
mx::array array;
};

inline fine::ResourcePtr<Tensor> wrap(mx::array a) {
return fine::make_resource<Tensor>(std::move(a));
}

inline mx::Shape to_mlx_shape(const std::vector<int64_t> &dims) {
mx::Shape out;
out.reserve(dims.size());
for (auto d : dims) {
if (d < 0) {
throw std::invalid_argument("negative dimension: " + std::to_string(d));
}
out.push_back(static_cast<mx::ShapeElem>(d));
}
return out;
}

inline std::vector<int> to_int_vec(const std::vector<int64_t> &v) {
return std::vector<int>(v.begin(), v.end());
}

inline std::vector<mx::array>
unwrap_all(const std::vector<fine::ResourcePtr<Tensor>> &tensors) {
std::vector<mx::array> out;
out.reserve(tensors.size());
for (const auto &t : tensors) {
out.push_back(t->array);
}
return out;
}

} // namespace emily
90 changes: 24 additions & 66 deletions c_src/emily_nif.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// emily_nif.cpp — minimal M0 surface: tensor round-trip.
// emily_nif.cpp — core NIFs: tensor resource, round-trip, eval.
//
// The Tensor resource wraps an mlx::core::array. MLX arrays are
// reference-counted internally; our ResourcePtr<Tensor> just adds one
// BEAM-managed ref. No manual atomics, no custom destructor — fine and
// MLX together do the right thing.
// Op NIFs live in c_src/ops/*.cpp; they share the Tensor resource
// defined here via emily/tensor.hpp.

#include "emily/tensor.hpp"

#include <fine.hpp>
#include <mlx/mlx.h>
Expand All @@ -12,63 +12,19 @@
#include <cstring>
#include <stdexcept>
#include <string>
#include <tuple>
#include <vector>

namespace mx = mlx::core;

// ---------- dtype mapping ----------

namespace {

mx::Dtype to_mlx_dtype(const std::string &kind, int64_t bits) {
if (kind == "f" && bits == 32) return mx::float32;
if (kind == "f" && bits == 16) return mx::float16;
if (kind == "bf" && bits == 16) return mx::bfloat16;
if (kind == "s" && bits == 8) return mx::int8;
if (kind == "s" && bits == 16) return mx::int16;
if (kind == "s" && bits == 32) return mx::int32;
if (kind == "s" && bits == 64) return mx::int64;
if (kind == "u" && bits == 8) return mx::uint8;
if (kind == "u" && bits == 16) return mx::uint16;
if (kind == "u" && bits == 32) return mx::uint32;
if (kind == "u" && bits == 64) return mx::uint64;
if (kind == "c" && bits == 64) return mx::complex64;
if (kind == "pred") return mx::bool_;

throw std::invalid_argument(
"unsupported dtype: {" + kind + ", " + std::to_string(bits) + "}");
}

std::tuple<fine::Atom, int64_t> from_mlx_dtype(mx::Dtype dtype) {
if (dtype == mx::float32) return {fine::Atom("f"), 32};
if (dtype == mx::float16) return {fine::Atom("f"), 16};
if (dtype == mx::bfloat16) return {fine::Atom("bf"), 16};
if (dtype == mx::int8) return {fine::Atom("s"), 8};
if (dtype == mx::int16) return {fine::Atom("s"), 16};
if (dtype == mx::int32) return {fine::Atom("s"), 32};
if (dtype == mx::int64) return {fine::Atom("s"), 64};
if (dtype == mx::uint8) return {fine::Atom("u"), 8};
if (dtype == mx::uint16) return {fine::Atom("u"), 16};
if (dtype == mx::uint32) return {fine::Atom("u"), 32};
if (dtype == mx::uint64) return {fine::Atom("u"), 64};
if (dtype == mx::complex64) return {fine::Atom("c"), 64};
if (dtype == mx::bool_) return {fine::Atom("pred"), 1};
throw std::runtime_error("unmapped mlx dtype");
}

} // namespace

// ---------- Tensor resource ----------

class Tensor {
public:
Tensor(mx::array a) : array(std::move(a)) {}
mx::array array;
};
using emily::Tensor;
using emily::from_mlx_dtype;
using emily::to_mlx_dtype;
using emily::to_mlx_shape;
using emily::wrap;

FINE_RESOURCE(Tensor);

// ---------- NIFs ----------
// ---------- Core NIFs ----------

// from_binary/3 — build a lazy MLX array from a BEAM binary.
// Regular scheduler: MLX copies the buffer into its own storage during
Expand All @@ -79,15 +35,11 @@ fine::ResourcePtr<Tensor> from_binary(
std::vector<int64_t> shape,
std::tuple<fine::Atom, int64_t> dtype_tuple) {

auto kind = std::get<0>(dtype_tuple).to_string();
auto bits = std::get<1>(dtype_tuple);
auto dtype = to_mlx_dtype(kind, bits);

std::vector<int> shape_ints(shape.begin(), shape.end());
auto dtype = to_mlx_dtype(dtype_tuple);
auto shape_ints = to_mlx_shape(shape);

int64_t nelem = 1;
for (auto d : shape_ints) {
if (d < 0) throw std::invalid_argument("negative dimension");
nelem *= d;
}

Expand All @@ -106,17 +58,23 @@ fine::ResourcePtr<Tensor> from_binary(
auto deleter = [](mx::allocator::Buffer b) { mx::allocator::free(b); };

mx::array arr(buf, std::move(shape_ints), dtype, deleter);
return fine::make_resource<Tensor>(std::move(arr));
return wrap(std::move(arr));
}
FINE_NIF(from_binary, 0);

// to_binary/1 — materialize the array and return its bytes as a binary.
// Dirty CPU: eval() triggers kernel launch and waits for completion.
//
// We route through mx::contiguous() first so views with non-standard
// strides (transpose, slice, swapaxes, broadcast_to) produce the
// correct in-memory layout. For already-contiguous arrays MLX elides
// the copy.
std::string to_binary(ErlNifEnv *, fine::ResourcePtr<Tensor> tensor) {
mx::eval(tensor->array);
auto materialized = mx::contiguous(tensor->array);
mx::eval(materialized);

const void *src = tensor->array.data<void>();
size_t nbytes = tensor->array.nbytes();
const void *src = materialized.data<void>();
size_t nbytes = materialized.nbytes();

std::string out;
out.resize(nbytes);
Expand Down
57 changes: 57 additions & 0 deletions c_src/ops/binary.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Binary elementwise: arithmetic, compare, logical, bitwise.

#include "../emily/tensor.hpp"

#include <fine.hpp>
#include <mlx/mlx.h>

namespace mx = mlx::core;
using emily::Tensor;
using emily::wrap;

namespace {

#define EMILY_BINARY(nif_name, mlx_fn) \
fine::ResourcePtr<Tensor> nif_name( \
ErlNifEnv *, \
fine::ResourcePtr<Tensor> a, \
fine::ResourcePtr<Tensor> b) { \
return wrap(mlx_fn(a->array, b->array)); \
} \
FINE_NIF(nif_name, 0);

// Arithmetic
EMILY_BINARY(add, mx::add)
EMILY_BINARY(subtract, mx::subtract)
EMILY_BINARY(multiply, mx::multiply)
EMILY_BINARY(divide, mx::divide)
EMILY_BINARY(floor_divide, mx::floor_divide)
EMILY_BINARY(remainder, mx::remainder)
EMILY_BINARY(power, mx::power)
EMILY_BINARY(maximum, mx::maximum)
EMILY_BINARY(minimum, mx::minimum)
EMILY_BINARY(logaddexp, mx::logaddexp)
EMILY_BINARY(arctan2, mx::arctan2)

// Compare
EMILY_BINARY(equal, mx::equal)
EMILY_BINARY(not_equal, mx::not_equal)
EMILY_BINARY(less, mx::less)
EMILY_BINARY(less_equal, mx::less_equal)
EMILY_BINARY(greater, mx::greater)
EMILY_BINARY(greater_equal, mx::greater_equal)

// Logical
EMILY_BINARY(logical_and, mx::logical_and)
EMILY_BINARY(logical_or, mx::logical_or)

// Bitwise
EMILY_BINARY(bitwise_and, mx::bitwise_and)
EMILY_BINARY(bitwise_or, mx::bitwise_or)
EMILY_BINARY(bitwise_xor, mx::bitwise_xor)
EMILY_BINARY(left_shift, mx::left_shift)
EMILY_BINARY(right_shift, mx::right_shift)

#undef EMILY_BINARY

} // namespace
Loading
Loading