Skip to content

Commit a6c949e

Browse files
committed
M1: remaining op inventory + soak + dtype smoke matrix
Extend Emily.Native with the ops needed to close M1: - sort/topk: sort, argsort, partition, argpartition, topk - axis-aligned gather/scatter: take_along_axis, put_along_axis, scatter_add_axis - misc: clip, roll, softmax, logcumsumexp, array_equal - conv_general: full-featured N-D convolution (padding bundled as a {lo, hi} tuple to keep arity credo-clean) - random: random_key, random_split, random_uniform, random_normal, random_randint, random_bernoulli, random_gumbel, random_categorical; keys carried through as std::optional<Tensor> so nil uses MLX's default key sequence - fft: fftn, ifftn, rfftn, irfftn - memory: get_active_memory, get_peak_memory, reset_peak_memory, get_cache_memory, clear_cache — used by the soak harness to observe allocator state Tests: - test/support/tensor_helpers.ex: extract f32/s32/pred builders, flat-list readers, and assert_close into a shared module. - test/emily/native_test.exs: refactor to import the shared helpers; add per-op unit tests for every new NIF. - test/soak/memory_test.exs (@tag :soak): 5000-iteration allocate/eval/ drop loop; asserts MLX active memory returns within 1 MB of baseline after clear_cache. Excluded from default mix test via test_helper. - test/emily/dtype_matrix_test.exs: smoke matrix covering every supported dtype across creation, cast, unary (float + numeric), binary, reductions, and comparisons. All M1 exit criteria now met: every MLX op we care about is callable from Elixir with correct outputs, the dtype × op surface is verified, and the memory soak shows no leaks under 5k iterations.
1 parent 9eda6ad commit a6c949e

13 files changed

Lines changed: 1093 additions & 63 deletions

File tree

RELEASE.md

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
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/`:
8+
- M1 — `Emily.Native` op inventory. Shared headers in `c_src/emily/`
9+
(dtype mapping, Tensor resource, helpers); per-category op files
10+
under `c_src/ops/`:
1111
- Creation: `zeros`, `ones`, `full`, `arange`, `eye`.
1212
- Cast: `astype`.
1313
- Unary elementwise: `negative`, `abs`, `sign`, `floor`, `ceil`,
@@ -26,24 +26,46 @@
2626
`right_shift`.
2727
- Reductions (axes + keepdims): `sum`, `mean`, `prod`, `max`, `min`,
2828
`all`, `any`, `logsumexp`; plus `var`/`std` with `ddof`,
29-
`argmax`/`argmin`, cumulative `cumsum`/`cumprod`/`cummax`/`cummin`.
29+
`argmax`/`argmin`, cumulative `cumsum`/`cumprod`/`cummax`/`cummin`/
30+
`logcumsumexp`.
3031
- Shape: `reshape`, `transpose`, `squeeze`, `expand_dims`,
3132
`broadcast_to`, `concatenate`, `stack`, `flatten`, `tile`,
3233
`swapaxes`, `pad`, `repeat`.
33-
- Indexing: `slice`, `take`, `where`.
34+
- Sort family: `sort`, `argsort`, `partition`, `argpartition`,
35+
`topk`.
36+
- Indexing: `slice`, `take`, `where`, `take_along_axis`,
37+
`put_along_axis`, `scatter_add_axis`.
38+
- Misc: `clip`, `roll`, `softmax`, `array_equal`.
3439
- Linalg: `matmul`, `tensordot`, `outer`, `inner`.
35-
- `Emily.Native.to_binary/1` now routes through `mx::contiguous` so
40+
- Convolution: `conv_general` (N-D with asymmetric padding,
41+
dilation, groups, flip).
42+
- Random: `random_key`, `random_split`, `random_uniform`,
43+
`random_normal`, `random_randint`, `random_bernoulli`,
44+
`random_gumbel`, `random_categorical` — keys passed as optional
45+
tensor args (nil uses MLX's default key sequence).
46+
- FFT: `fftn`, `ifftn`, `rfftn`, `irfftn`.
47+
- Memory: `get_active_memory`, `get_peak_memory`,
48+
`reset_peak_memory`, `get_cache_memory`, `clear_cache` — exposed
49+
so the soak harness can observe allocator state.
50+
- `Emily.Native.to_binary/1` routes through `mx::contiguous` so
3651
strided views (transpose/slice/swapaxes/broadcast) materialize
3752
correctly.
53+
- `test/support/tensor_helpers.ex` — shared build/inspect helpers.
54+
- `test/soak/memory_test.exs` (`@tag :soak`, excluded by default) —
55+
5000-iteration allocate/eval/drop loop; asserts MLX active memory
56+
returns within 1 MB of baseline after `clear_cache`.
57+
- `test/emily/dtype_matrix_test.exs` — smoke matrix covering every
58+
supported dtype across creation, cast, unary (float + numeric),
59+
binary, reductions, and comparisons.
3860
- Makefile compiles `c_src/**/*.cpp` recursively.
3961

4062
## Notes
4163

4264
- Ops files use anonymous namespaces to prevent NIF function names
4365
(`sin`, `log1p`, `sqrt`, ...) from colliding with C math-library
4466
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).
67+
- Deferred beyond M1: the full `scatter`/`scatter_add`/... family with
68+
vector-of-indices (only the axis-aligned forms are bound),
69+
`hadamard_transform`, quantized matmul, `linalg.*` decompositions
70+
(LU, QR, Cholesky, SVD). These will be added opportunistically when
71+
M2/M3 callers need them.

c_src/memory.cpp

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// MLX allocator introspection — exposed as NIFs so the soak harness
2+
// can observe allocator state and assert it returns to baseline.
3+
4+
#include <fine.hpp>
5+
#include <mlx/mlx.h>
6+
7+
#include <cstdint>
8+
9+
namespace mx = mlx::core;
10+
11+
namespace {
12+
13+
int64_t get_active_memory(ErlNifEnv *) {
14+
return static_cast<int64_t>(mx::get_active_memory());
15+
}
16+
FINE_NIF(get_active_memory, 0);
17+
18+
int64_t get_peak_memory(ErlNifEnv *) {
19+
return static_cast<int64_t>(mx::get_peak_memory());
20+
}
21+
FINE_NIF(get_peak_memory, 0);
22+
23+
fine::Ok<> reset_peak_memory(ErlNifEnv *) {
24+
mx::reset_peak_memory();
25+
return fine::Ok<>{};
26+
}
27+
FINE_NIF(reset_peak_memory, 0);
28+
29+
int64_t get_cache_memory(ErlNifEnv *) {
30+
return static_cast<int64_t>(mx::get_cache_memory());
31+
}
32+
FINE_NIF(get_cache_memory, 0);
33+
34+
fine::Ok<> clear_cache(ErlNifEnv *) {
35+
mx::clear_cache();
36+
return fine::Ok<>{};
37+
}
38+
FINE_NIF(clear_cache, 0);
39+
40+
} // namespace

c_src/ops/conv.cpp

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Convolutions.
2+
//
3+
// Only the most general form (`conv_general`) is bound; 1-D/2-D/3-D
4+
// specialisations can be layered in Elixir by picking strides/padding
5+
// vectors of the right arity. This mirrors how Nx.conv/2 translates.
6+
7+
#include "../emily/tensor.hpp"
8+
9+
#include <fine.hpp>
10+
#include <mlx/mlx.h>
11+
12+
#include <cstdint>
13+
#include <vector>
14+
15+
namespace mx = mlx::core;
16+
using emily::Tensor;
17+
using emily::to_int_vec;
18+
using emily::wrap;
19+
20+
namespace {
21+
22+
// `padding` bundles low/high padding into a single tuple so the NIF
23+
// arity stays manageable; both have length == spatial rank.
24+
fine::ResourcePtr<Tensor> conv_general(
25+
ErlNifEnv *,
26+
fine::ResourcePtr<Tensor> input,
27+
fine::ResourcePtr<Tensor> weight,
28+
std::vector<int64_t> stride,
29+
std::tuple<std::vector<int64_t>, std::vector<int64_t>> padding,
30+
std::vector<int64_t> kernel_dilation,
31+
std::vector<int64_t> input_dilation,
32+
int64_t groups,
33+
bool flip) {
34+
return wrap(mx::conv_general(
35+
input->array,
36+
weight->array,
37+
to_int_vec(stride),
38+
to_int_vec(std::get<0>(padding)),
39+
to_int_vec(std::get<1>(padding)),
40+
to_int_vec(kernel_dilation),
41+
to_int_vec(input_dilation),
42+
static_cast<int>(groups),
43+
flip));
44+
}
45+
FINE_NIF(conv_general, 0);
46+
47+
} // namespace

c_src/ops/fft.cpp

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Fast Fourier Transforms.
2+
//
3+
// We bind the n-dimensional forms; 1-D/2-D specialisations can be
4+
// built on top by picking axis vectors of the right length.
5+
6+
#include "../emily/tensor.hpp"
7+
8+
#include <fine.hpp>
9+
#include <mlx/mlx.h>
10+
11+
#include <cstdint>
12+
#include <vector>
13+
14+
namespace mx = mlx::core;
15+
namespace fft = mlx::core::fft;
16+
using emily::Tensor;
17+
using emily::to_int_vec;
18+
using emily::to_mlx_shape;
19+
using emily::wrap;
20+
21+
namespace {
22+
23+
fine::ResourcePtr<Tensor> fftn(
24+
ErlNifEnv *,
25+
fine::ResourcePtr<Tensor> a,
26+
std::vector<int64_t> n,
27+
std::vector<int64_t> axes) {
28+
return wrap(fft::fftn(a->array, to_mlx_shape(n), to_int_vec(axes)));
29+
}
30+
FINE_NIF(fftn, 0);
31+
32+
fine::ResourcePtr<Tensor> ifftn(
33+
ErlNifEnv *,
34+
fine::ResourcePtr<Tensor> a,
35+
std::vector<int64_t> n,
36+
std::vector<int64_t> axes) {
37+
return wrap(fft::ifftn(a->array, to_mlx_shape(n), to_int_vec(axes)));
38+
}
39+
FINE_NIF(ifftn, 0);
40+
41+
fine::ResourcePtr<Tensor> rfftn(
42+
ErlNifEnv *,
43+
fine::ResourcePtr<Tensor> a,
44+
std::vector<int64_t> n,
45+
std::vector<int64_t> axes) {
46+
return wrap(fft::rfftn(a->array, to_mlx_shape(n), to_int_vec(axes)));
47+
}
48+
FINE_NIF(rfftn, 0);
49+
50+
fine::ResourcePtr<Tensor> irfftn(
51+
ErlNifEnv *,
52+
fine::ResourcePtr<Tensor> a,
53+
std::vector<int64_t> n,
54+
std::vector<int64_t> axes) {
55+
return wrap(fft::irfftn(a->array, to_mlx_shape(n), to_int_vec(axes)));
56+
}
57+
FINE_NIF(irfftn, 0);
58+
59+
} // namespace

c_src/ops/index.cpp

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,42 @@ fine::ResourcePtr<Tensor> where(
5050
}
5151
FINE_NIF(where, 0);
5252

53+
// take_along_axis/3 — gather along `axis` using integer indices whose
54+
// shape matches `a` except along `axis`.
55+
fine::ResourcePtr<Tensor> take_along_axis(
56+
ErlNifEnv *,
57+
fine::ResourcePtr<Tensor> a,
58+
fine::ResourcePtr<Tensor> indices,
59+
int64_t axis) {
60+
return wrap(mx::take_along_axis(
61+
a->array, indices->array, static_cast<int>(axis)));
62+
}
63+
FINE_NIF(take_along_axis, 0);
64+
65+
// put_along_axis/4 — write `values` into `a` at the given indices
66+
// along `axis`.
67+
fine::ResourcePtr<Tensor> put_along_axis(
68+
ErlNifEnv *,
69+
fine::ResourcePtr<Tensor> a,
70+
fine::ResourcePtr<Tensor> indices,
71+
fine::ResourcePtr<Tensor> values,
72+
int64_t axis) {
73+
return wrap(mx::put_along_axis(
74+
a->array, indices->array, values->array, static_cast<int>(axis)));
75+
}
76+
FINE_NIF(put_along_axis, 0);
77+
78+
// scatter_add_axis/4 — add `values` into `a` at the given indices
79+
// along `axis`.
80+
fine::ResourcePtr<Tensor> scatter_add_axis(
81+
ErlNifEnv *,
82+
fine::ResourcePtr<Tensor> a,
83+
fine::ResourcePtr<Tensor> indices,
84+
fine::ResourcePtr<Tensor> values,
85+
int64_t axis) {
86+
return wrap(mx::scatter_add_axis(
87+
a->array, indices->array, values->array, static_cast<int>(axis)));
88+
}
89+
FINE_NIF(scatter_add_axis, 0);
90+
5391
} // namespace

c_src/ops/misc.cpp

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Miscellaneous ops that didn't fit elsewhere: clip, roll, softmax,
2+
// logcumsumexp, array_equal.
3+
4+
#include "../emily/tensor.hpp"
5+
6+
#include <fine.hpp>
7+
#include <mlx/mlx.h>
8+
9+
#include <cstdint>
10+
#include <vector>
11+
12+
namespace mx = mlx::core;
13+
using emily::Tensor;
14+
using emily::to_int_vec;
15+
using emily::wrap;
16+
17+
namespace {
18+
19+
// clip/3 — clip each element to [min, max]. Both bounds are required
20+
// tensors; the caller can broadcast a scalar if only one side is
21+
// interesting.
22+
fine::ResourcePtr<Tensor> clip(
23+
ErlNifEnv *,
24+
fine::ResourcePtr<Tensor> a,
25+
fine::ResourcePtr<Tensor> a_min,
26+
fine::ResourcePtr<Tensor> a_max) {
27+
return wrap(mx::clip(a->array, a_min->array, a_max->array));
28+
}
29+
FINE_NIF(clip, 0);
30+
31+
// roll/3 — shift elements `shift` steps along `axis` with wrap-around.
32+
fine::ResourcePtr<Tensor> roll(
33+
ErlNifEnv *,
34+
fine::ResourcePtr<Tensor> a,
35+
int64_t shift,
36+
int64_t axis) {
37+
return wrap(mx::roll(
38+
a->array, static_cast<int>(shift), static_cast<int>(axis)));
39+
}
40+
FINE_NIF(roll, 0);
41+
42+
// softmax along the given axes.
43+
fine::ResourcePtr<Tensor> softmax(
44+
ErlNifEnv *,
45+
fine::ResourcePtr<Tensor> a,
46+
std::vector<int64_t> axes,
47+
bool precise) {
48+
return wrap(mx::softmax(a->array, to_int_vec(axes), precise));
49+
}
50+
FINE_NIF(softmax, 0);
51+
52+
fine::ResourcePtr<Tensor> logcumsumexp(
53+
ErlNifEnv *,
54+
fine::ResourcePtr<Tensor> a,
55+
int64_t axis,
56+
bool reverse,
57+
bool inclusive) {
58+
return wrap(mx::logcumsumexp(
59+
a->array, static_cast<int>(axis), reverse, inclusive));
60+
}
61+
FINE_NIF(logcumsumexp, 0);
62+
63+
// array_equal/2 — returns a scalar bool tensor. Treats NaNs as unequal.
64+
fine::ResourcePtr<Tensor> array_equal(
65+
ErlNifEnv *,
66+
fine::ResourcePtr<Tensor> a,
67+
fine::ResourcePtr<Tensor> b,
68+
bool equal_nan) {
69+
return wrap(mx::array_equal(a->array, b->array, equal_nan));
70+
}
71+
FINE_NIF(array_equal, 0);
72+
73+
} // namespace

0 commit comments

Comments
 (0)