Skip to content

Commit 9eda6ad

Browse files
authored
Merge pull request #4 from ausimian/feat/m1-native-ops
M1 (partial): Emily.Native op inventory
2 parents b72a2f6 + 203e859 commit 9eda6ad

15 files changed

Lines changed: 1531 additions & 99 deletions

File tree

Makefile

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ MLX_STAGE_DIR := $(PRIV_DIR)/mlx/lib
44

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

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

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

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

4647
$(MLX_STAGE_DIR): | $(PRIV_DIR)

RELEASE.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,45 @@
55
- M0 scaffold: mix project, MLX 0.25.1 prebuilt fetch pipeline,
66
Makefile wiring `fine` + MLX, `Emily.Native` NIF surface for tensor
77
round-trip, application supervisor skeleton, smoke test suite.
8+
- M1 (partial) — `Emily.Native` op inventory. Shared headers in
9+
`c_src/emily/` (dtype mapping, Tensor resource, helpers);
10+
per-category op files under `c_src/ops/`:
11+
- Creation: `zeros`, `ones`, `full`, `arange`, `eye`.
12+
- Cast: `astype`.
13+
- Unary elementwise: `negative`, `abs`, `sign`, `floor`, `ceil`,
14+
`sqrt`, `rsqrt`, `exp`, `expm1`, `log`, `log1p`, `log2`, `log10`,
15+
trig/inverse-trig/hyperbolic family, `sigmoid`, `erf`, `erfinv`,
16+
`square`, `reciprocal`, `logical_not`, `bitwise_invert`, `isnan`,
17+
`isinf`, `isfinite`, `conjugate`, `real`, `imag`, `stop_gradient`,
18+
`round` (with decimals).
19+
- Binary elementwise: `add`, `subtract`, `multiply`, `divide`,
20+
`floor_divide`, `remainder`, `power`, `maximum`, `minimum`,
21+
`logaddexp`, `arctan2`.
22+
- Compare: `equal`, `not_equal`, `less`, `less_equal`, `greater`,
23+
`greater_equal`.
24+
- Logical: `logical_and`, `logical_or`.
25+
- Bitwise: `bitwise_and`, `bitwise_or`, `bitwise_xor`, `left_shift`,
26+
`right_shift`.
27+
- Reductions (axes + keepdims): `sum`, `mean`, `prod`, `max`, `min`,
28+
`all`, `any`, `logsumexp`; plus `var`/`std` with `ddof`,
29+
`argmax`/`argmin`, cumulative `cumsum`/`cumprod`/`cummax`/`cummin`.
30+
- Shape: `reshape`, `transpose`, `squeeze`, `expand_dims`,
31+
`broadcast_to`, `concatenate`, `stack`, `flatten`, `tile`,
32+
`swapaxes`, `pad`, `repeat`.
33+
- Indexing: `slice`, `take`, `where`.
34+
- Linalg: `matmul`, `tensordot`, `outer`, `inner`.
35+
- `Emily.Native.to_binary/1` now routes through `mx::contiguous` so
36+
strided views (transpose/slice/swapaxes/broadcast) materialize
37+
correctly.
38+
- Makefile compiles `c_src/**/*.cpp` recursively.
39+
40+
## Notes
41+
42+
- Ops files use anonymous namespaces to prevent NIF function names
43+
(`sin`, `log1p`, `sqrt`, ...) from colliding with C math-library
44+
symbols pulled in by MLX headers.
45+
- Deferred to later iterations of M1: sort/argsort, clip,
46+
`slice_update`, `take_along_axis`, `scatter*`, convolutions,
47+
`hadamard_transform`, random ops, FFT, quantized ops,
48+
memory-stats/soak tests. Tracked for M1 completion before moving to
49+
M2 (Backend).

c_src/emily/dtype.hpp

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Dtype translation between Nx's {kind, bits} tuples and mlx::Dtype.
2+
//
3+
// Nx kinds we honour: "f" (float), "bf" (bfloat), "s" (signed int),
4+
// "u" (unsigned int), "c" (complex), "pred" (1-bit bool, MLX stores
5+
// as one byte).
6+
7+
#pragma once
8+
9+
#include <fine.hpp>
10+
#include <mlx/mlx.h>
11+
12+
#include <cstdint>
13+
#include <stdexcept>
14+
#include <string>
15+
#include <tuple>
16+
17+
namespace emily {
18+
19+
namespace mx = mlx::core;
20+
21+
inline mx::Dtype to_mlx_dtype(const std::string &kind, int64_t bits) {
22+
if (kind == "f" && bits == 32) return mx::float32;
23+
if (kind == "f" && bits == 16) return mx::float16;
24+
if (kind == "bf" && bits == 16) return mx::bfloat16;
25+
if (kind == "s" && bits == 8) return mx::int8;
26+
if (kind == "s" && bits == 16) return mx::int16;
27+
if (kind == "s" && bits == 32) return mx::int32;
28+
if (kind == "s" && bits == 64) return mx::int64;
29+
if (kind == "u" && bits == 8) return mx::uint8;
30+
if (kind == "u" && bits == 16) return mx::uint16;
31+
if (kind == "u" && bits == 32) return mx::uint32;
32+
if (kind == "u" && bits == 64) return mx::uint64;
33+
if (kind == "c" && bits == 64) return mx::complex64;
34+
if (kind == "pred") return mx::bool_;
35+
36+
throw std::invalid_argument(
37+
"unsupported dtype: {" + kind + ", " + std::to_string(bits) + "}");
38+
}
39+
40+
inline mx::Dtype to_mlx_dtype(const std::tuple<fine::Atom, int64_t> &t) {
41+
return to_mlx_dtype(std::get<0>(t).to_string(), std::get<1>(t));
42+
}
43+
44+
inline std::tuple<fine::Atom, int64_t> from_mlx_dtype(mx::Dtype dtype) {
45+
if (dtype == mx::float32) return {fine::Atom("f"), 32};
46+
if (dtype == mx::float16) return {fine::Atom("f"), 16};
47+
if (dtype == mx::bfloat16) return {fine::Atom("bf"), 16};
48+
if (dtype == mx::int8) return {fine::Atom("s"), 8};
49+
if (dtype == mx::int16) return {fine::Atom("s"), 16};
50+
if (dtype == mx::int32) return {fine::Atom("s"), 32};
51+
if (dtype == mx::int64) return {fine::Atom("s"), 64};
52+
if (dtype == mx::uint8) return {fine::Atom("u"), 8};
53+
if (dtype == mx::uint16) return {fine::Atom("u"), 16};
54+
if (dtype == mx::uint32) return {fine::Atom("u"), 32};
55+
if (dtype == mx::uint64) return {fine::Atom("u"), 64};
56+
if (dtype == mx::complex64) return {fine::Atom("c"), 64};
57+
if (dtype == mx::bool_) return {fine::Atom("pred"), 1};
58+
throw std::runtime_error("unmapped mlx dtype");
59+
}
60+
61+
} // namespace emily

c_src/emily/tensor.hpp

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Tensor: opaque resource wrapping mlx::core::array.
2+
//
3+
// MLX arrays are refcounted internally; our ResourcePtr<Tensor> adds
4+
// one BEAM-managed ref. No manual atomics, no custom destructor — fine
5+
// and MLX together do the right thing.
6+
//
7+
// Helpers: wrap/unwrap shortcuts + shape conversion between Nx's
8+
// list-of-int64 format and MLX's std::vector<int32_t> Shape.
9+
10+
#pragma once
11+
12+
#include "dtype.hpp"
13+
14+
#include <fine.hpp>
15+
#include <mlx/mlx.h>
16+
17+
#include <cstdint>
18+
#include <stdexcept>
19+
#include <string>
20+
#include <vector>
21+
22+
namespace emily {
23+
24+
namespace mx = mlx::core;
25+
26+
class Tensor {
27+
public:
28+
Tensor(mx::array a) : array(std::move(a)) {}
29+
mx::array array;
30+
};
31+
32+
inline fine::ResourcePtr<Tensor> wrap(mx::array a) {
33+
return fine::make_resource<Tensor>(std::move(a));
34+
}
35+
36+
inline mx::Shape to_mlx_shape(const std::vector<int64_t> &dims) {
37+
mx::Shape out;
38+
out.reserve(dims.size());
39+
for (auto d : dims) {
40+
if (d < 0) {
41+
throw std::invalid_argument("negative dimension: " + std::to_string(d));
42+
}
43+
out.push_back(static_cast<mx::ShapeElem>(d));
44+
}
45+
return out;
46+
}
47+
48+
inline std::vector<int> to_int_vec(const std::vector<int64_t> &v) {
49+
return std::vector<int>(v.begin(), v.end());
50+
}
51+
52+
inline std::vector<mx::array>
53+
unwrap_all(const std::vector<fine::ResourcePtr<Tensor>> &tensors) {
54+
std::vector<mx::array> out;
55+
out.reserve(tensors.size());
56+
for (const auto &t : tensors) {
57+
out.push_back(t->array);
58+
}
59+
return out;
60+
}
61+
62+
} // namespace emily

c_src/emily_nif.cpp

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

88
#include <fine.hpp>
99
#include <mlx/mlx.h>
@@ -12,63 +12,19 @@
1212
#include <cstring>
1313
#include <stdexcept>
1414
#include <string>
15+
#include <tuple>
1516
#include <vector>
1617

1718
namespace mx = mlx::core;
18-
19-
// ---------- dtype mapping ----------
20-
21-
namespace {
22-
23-
mx::Dtype to_mlx_dtype(const std::string &kind, int64_t bits) {
24-
if (kind == "f" && bits == 32) return mx::float32;
25-
if (kind == "f" && bits == 16) return mx::float16;
26-
if (kind == "bf" && bits == 16) return mx::bfloat16;
27-
if (kind == "s" && bits == 8) return mx::int8;
28-
if (kind == "s" && bits == 16) return mx::int16;
29-
if (kind == "s" && bits == 32) return mx::int32;
30-
if (kind == "s" && bits == 64) return mx::int64;
31-
if (kind == "u" && bits == 8) return mx::uint8;
32-
if (kind == "u" && bits == 16) return mx::uint16;
33-
if (kind == "u" && bits == 32) return mx::uint32;
34-
if (kind == "u" && bits == 64) return mx::uint64;
35-
if (kind == "c" && bits == 64) return mx::complex64;
36-
if (kind == "pred") return mx::bool_;
37-
38-
throw std::invalid_argument(
39-
"unsupported dtype: {" + kind + ", " + std::to_string(bits) + "}");
40-
}
41-
42-
std::tuple<fine::Atom, int64_t> from_mlx_dtype(mx::Dtype dtype) {
43-
if (dtype == mx::float32) return {fine::Atom("f"), 32};
44-
if (dtype == mx::float16) return {fine::Atom("f"), 16};
45-
if (dtype == mx::bfloat16) return {fine::Atom("bf"), 16};
46-
if (dtype == mx::int8) return {fine::Atom("s"), 8};
47-
if (dtype == mx::int16) return {fine::Atom("s"), 16};
48-
if (dtype == mx::int32) return {fine::Atom("s"), 32};
49-
if (dtype == mx::int64) return {fine::Atom("s"), 64};
50-
if (dtype == mx::uint8) return {fine::Atom("u"), 8};
51-
if (dtype == mx::uint16) return {fine::Atom("u"), 16};
52-
if (dtype == mx::uint32) return {fine::Atom("u"), 32};
53-
if (dtype == mx::uint64) return {fine::Atom("u"), 64};
54-
if (dtype == mx::complex64) return {fine::Atom("c"), 64};
55-
if (dtype == mx::bool_) return {fine::Atom("pred"), 1};
56-
throw std::runtime_error("unmapped mlx dtype");
57-
}
58-
59-
} // namespace
60-
61-
// ---------- Tensor resource ----------
62-
63-
class Tensor {
64-
public:
65-
Tensor(mx::array a) : array(std::move(a)) {}
66-
mx::array array;
67-
};
19+
using emily::Tensor;
20+
using emily::from_mlx_dtype;
21+
using emily::to_mlx_dtype;
22+
using emily::to_mlx_shape;
23+
using emily::wrap;
6824

6925
FINE_RESOURCE(Tensor);
7026

71-
// ---------- NIFs ----------
27+
// ---------- Core NIFs ----------
7228

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

82-
auto kind = std::get<0>(dtype_tuple).to_string();
83-
auto bits = std::get<1>(dtype_tuple);
84-
auto dtype = to_mlx_dtype(kind, bits);
85-
86-
std::vector<int> shape_ints(shape.begin(), shape.end());
38+
auto dtype = to_mlx_dtype(dtype_tuple);
39+
auto shape_ints = to_mlx_shape(shape);
8740

8841
int64_t nelem = 1;
8942
for (auto d : shape_ints) {
90-
if (d < 0) throw std::invalid_argument("negative dimension");
9143
nelem *= d;
9244
}
9345

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

10860
mx::array arr(buf, std::move(shape_ints), dtype, deleter);
109-
return fine::make_resource<Tensor>(std::move(arr));
61+
return wrap(std::move(arr));
11062
}
11163
FINE_NIF(from_binary, 0);
11264

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

118-
const void *src = tensor->array.data<void>();
119-
size_t nbytes = tensor->array.nbytes();
76+
const void *src = materialized.data<void>();
77+
size_t nbytes = materialized.nbytes();
12078

12179
std::string out;
12280
out.resize(nbytes);

c_src/ops/binary.cpp

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Binary elementwise: arithmetic, compare, logical, bitwise.
2+
3+
#include "../emily/tensor.hpp"
4+
5+
#include <fine.hpp>
6+
#include <mlx/mlx.h>
7+
8+
namespace mx = mlx::core;
9+
using emily::Tensor;
10+
using emily::wrap;
11+
12+
namespace {
13+
14+
#define EMILY_BINARY(nif_name, mlx_fn) \
15+
fine::ResourcePtr<Tensor> nif_name( \
16+
ErlNifEnv *, \
17+
fine::ResourcePtr<Tensor> a, \
18+
fine::ResourcePtr<Tensor> b) { \
19+
return wrap(mlx_fn(a->array, b->array)); \
20+
} \
21+
FINE_NIF(nif_name, 0);
22+
23+
// Arithmetic
24+
EMILY_BINARY(add, mx::add)
25+
EMILY_BINARY(subtract, mx::subtract)
26+
EMILY_BINARY(multiply, mx::multiply)
27+
EMILY_BINARY(divide, mx::divide)
28+
EMILY_BINARY(floor_divide, mx::floor_divide)
29+
EMILY_BINARY(remainder, mx::remainder)
30+
EMILY_BINARY(power, mx::power)
31+
EMILY_BINARY(maximum, mx::maximum)
32+
EMILY_BINARY(minimum, mx::minimum)
33+
EMILY_BINARY(logaddexp, mx::logaddexp)
34+
EMILY_BINARY(arctan2, mx::arctan2)
35+
36+
// Compare
37+
EMILY_BINARY(equal, mx::equal)
38+
EMILY_BINARY(not_equal, mx::not_equal)
39+
EMILY_BINARY(less, mx::less)
40+
EMILY_BINARY(less_equal, mx::less_equal)
41+
EMILY_BINARY(greater, mx::greater)
42+
EMILY_BINARY(greater_equal, mx::greater_equal)
43+
44+
// Logical
45+
EMILY_BINARY(logical_and, mx::logical_and)
46+
EMILY_BINARY(logical_or, mx::logical_or)
47+
48+
// Bitwise
49+
EMILY_BINARY(bitwise_and, mx::bitwise_and)
50+
EMILY_BINARY(bitwise_or, mx::bitwise_or)
51+
EMILY_BINARY(bitwise_xor, mx::bitwise_xor)
52+
EMILY_BINARY(left_shift, mx::left_shift)
53+
EMILY_BINARY(right_shift, mx::right_shift)
54+
55+
#undef EMILY_BINARY
56+
57+
} // namespace

0 commit comments

Comments
 (0)