Skip to content
Closed
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
1 change: 1 addition & 0 deletions ACKNOWLEDGMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ MLX was developed with contributions from the following individuals:
- Juarez Bochi: Fixed bug in cross attention.
- Justin Deschenaux: Sine, Cosine, arange, randint, truncated normal, bernoulli, lion optimizer, Dropout2d, linear and logistic regression python example.
- Diogo Da Cruz: Added `tri`, `tril`, `triu`, `tensordot` and safetensor support
- Bahaa Eddin Tabbakha: Added `repeat`, `tile`.

# Third-Party Software

Expand Down
1 change: 1 addition & 0 deletions docs/src/python/ops.rst
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ Operations
take_along_axis
tan
tanh
tile
tensordot
transpose
tri
Expand Down
37 changes: 37 additions & 0 deletions mlx/ops.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,43 @@ array repeat(const array& arr, int repeats, StreamOrDevice s) {
return repeat(flatten(arr, s), repeats, 0, s);
}

/** Replicating array a specified number of times along each axis */
array tile(const array& a, std::vector<int> repeats, StreamOrDevice s) {
if (repeats.size() == 0) {
return a;
}

for (size_t i = 0; i < repeats.size(); i++) {
if (repeats[i] < 0) {
throw std::invalid_argument(
"[tile] Negative repeat value found at index " + std::to_string(i));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extremely nitpicky, feel free to ignore. Using an ostringstream would be better as it would allow you to also provide the whole repeats vector. Something like:

std::ostringstream msg;
msg << "[tile] Repeats argument cannot contain negative values but "
    << repeats << " was provided.";
throw std::invalid_argument(msg.str());

}
}

int d = repeats.size();
array arr = copy(a, s);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy is not needed here, you can simply do array arr = a;.


if (d < arr.ndim()) {
repeats.insert(repeats.begin(), arr.ndim() - d, 1);
} else if (d > arr.ndim()) {
arr = expand_dims(arr, std::vector<int>(d - arr.ndim(), 0), s);
}

for (size_t i = 0; i < arr.shape().size(); i++) {
if (repeats[i] != 1) {
arr = expand_dims(arr, i, s);
std::vector<int> new_shape(arr.shape());
new_shape[i] *= repeats[i];
arr = broadcast_to(arr, new_shape, s);
new_shape[i] = -1;
new_shape.erase(new_shape.begin() + i + 1);
arr = reshape(arr, new_shape, s);
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already much better than before, however again each of these reshapes will be an actual copy and there is no need to launch many. Basically this is like calling repeat() many times.

The better way to do it is to keep 3 vectors.

  • a reshape vector which will do all the expands at once (start with a.shape() and add 1s where needed)
  • a broadcast to vector which will have the number of repeats where 1s were added in the above
  • final reshape vector where the repeats are merged to the axis to their left

After that the implementation is as simple as the following:

auto out = reshape(a, expand_shape, s);
out = broadcast_to(out, repeat_shape, s);
out = reshape(out, final_shape, s);


return arr;
}

/** Pad an array with a constant value */
array pad(
const array& a,
Expand Down
3 changes: 3 additions & 0 deletions mlx/ops.h
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,9 @@ array stack(const std::vector<array>& arrays, StreamOrDevice s = {});
array repeat(const array& arr, int repeats, int axis, StreamOrDevice s = {});
array repeat(const array& arr, int repeats, StreamOrDevice s = {});

/** Replicating array a specified number of times along each axis */
array tile(const array& arr, std::vector<int> repeats, StreamOrDevice s = {});

/** Permutes the dimensions according to the given axes. */
array transpose(const array& a, std::vector<int> axes, StreamOrDevice s = {});
inline array transpose(
Expand Down
23 changes: 23 additions & 0 deletions python/src/ops.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2439,6 +2439,29 @@ void init_ops(py::module_& m) {
Returns:
array: The resulting repeated array.
)pbdoc");
m.def(
"tile",
[](const array& array, std::vector<int> repeats, StreamOrDevice s) {
return tile(array, repeats, s);
},
"array"_a,
py::pos_only(),
"repeats"_a,
py::kw_only(),
"stream"_a = none,
R"pbdoc(
tile(array: array, repeats: array, *, stream: Union[None, Stream, Device] = None) -> array

Replicating array a specified number of times along each axis.

Args:
array (array): Input array.
repeats (array): The number of repetitions along each axis.
stream (Stream, optional): Stream or device. Defaults to ``None``.

Returns:
array: The resulting tiled array.
)pbdoc");
m.def(
"clip",
[](const array& a,
Expand Down
49 changes: 48 additions & 1 deletion python/tests/test_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -1518,7 +1518,10 @@ def test_linspace(self):

def test_repeat(self):
# Setup data for the tests
data = np.array([[[13, 3], [16, 6]], [[14, 4], [15, 5]], [[11, 1], [12, 2]]])
data = np.array(
[[[13, 3], [16, 6]], [[14, 4], [15, 5]], [[11, 1], [12, 2]]],
dtype=np.float32,
)
# Test repeat along axis 0
repeat_axis_0 = mx.repeat(mx.array(data), 2, axis=0)
expected_axis_0 = np.repeat(data, 2, axis=0)
Expand Down Expand Up @@ -1547,6 +1550,50 @@ def test_repeat(self):
expected_3 = np.repeat(data_3, 2, axis=0)
self.assertEqualArray(repeat_3, mx.array(expected_3))

def test_tile(self):
data = np.array(
[
[[6.0, 10.0], [1.0, 3.0]],
[[9.0, 5.0], [9.0, 2.0]],
[[5.0, 3.0], [3.0, 4.0]],
],
dtype=np.float32,
)

expected_1 = mx.tile(mx.array(data), [3])
tiled_1 = np.tile(data, [3])
self.assertEqualArray(expected_1, mx.array(tiled_1))

data_2 = np.array(
[
[[4.0, 4.0], [9.0, 7.0]],
[[7.0, 3.0], [6.0, 8.0]],
[[8.0, 3.0], [8.0, 7.0]],
],
dtype=np.float32,
)

expected_2 = mx.tile(mx.array(data_2), [1, 3, 2])
tiled_2 = np.tile(data_2, [1, 3, 2])
self.assertEqualArray(expected_2, mx.array(tiled_2))

expected_3 = mx.tile(mx.array(data_2), [3, 2, 2])
tiled_3 = np.tile(data_2, (3, 2, 2))
self.assertEqualArray(expected_3, mx.array(tiled_3))

expected_4 = mx.tile(mx.array(data_2), [3, 2, 2, 2])
tiled_4 = np.tile(data_2, (3, 2, 2, 2))
self.assertEqualArray(expected_4, mx.array(tiled_4))

expected_5 = mx.tile(mx.array(data_2), [1, 1, 1])
tiled_5 = np.tile(data_2, (1, 1, 1))
self.assertEqualArray(expected_5, mx.array(tiled_5))

expected_6 = mx.tile(mx.array(data_2), [1, 1, 1, 1])
tiled_6 = np.tile(data_2, (1, 1, 1, 1))
self.assertEqualArray(expected_6, mx.array(tiled_6))


def test_tensordot(self):
x = mx.arange(60.0).reshape(3, 4, 5)
y = mx.arange(24.0).reshape(4, 3, 2)
Expand Down
76 changes: 75 additions & 1 deletion tests/ops_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2279,6 +2279,80 @@ TEST_CASE("test repeat") {
CHECK_THROWS_AS(repeat(data_3, -3, 0), std::invalid_argument);
}

TEST_CASE("test tile") {
auto data =
array({6., 10., 1., 3., 9., 5., 9., 2., 5., 3., 3., 4.}, {3, 2, 2});
auto expected_1 = array(
{6., 10., 6., 10., 6., 10., 1., 3., 1., 3., 1., 3.,
9., 5., 9., 5., 9., 5., 9., 2., 9., 2., 9., 2.,
5., 3., 5., 3., 5., 3., 3., 4., 3., 4., 3., 4.},
{3, 2, 6});

auto tiled_1 = tile(data, {3});

auto data_2 =
array({4., 4., 9., 7., 7., 3., 6., 8., 8., 3., 8., 7.}, {3, 2, 2});

auto expected_2 = array(
{4., 4., 4., 4., 9., 7., 9., 7., 4., 4., 4., 4., 9., 7., 9., 7., 4., 4.,
4., 4., 9., 7., 9., 7., 7., 3., 7., 3., 6., 8., 6., 8., 7., 3., 7., 3.,
6., 8., 6., 8., 7., 3., 7., 3., 6., 8., 6., 8., 8., 3., 8., 3., 8., 7.,
8., 7., 8., 3., 8., 3., 8., 7., 8., 7., 8., 3., 8., 3., 8., 7., 8., 7.},
{3, 6, 4});

auto tiled_2 = tile(data_2, {3, 2});

auto expected_3 = array(
{4., 4., 4., 4., 9., 7., 9., 7., 4., 4., 4., 4., 9., 7., 9., 7., 7., 3.,
7., 3., 6., 8., 6., 8., 7., 3., 7., 3., 6., 8., 6., 8., 8., 3., 8., 3.,
8., 7., 8., 7., 8., 3., 8., 3., 8., 7., 8., 7., 4., 4., 4., 4., 9., 7.,
9., 7., 4., 4., 4., 4., 9., 7., 9., 7., 7., 3., 7., 3., 6., 8., 6., 8.,
7., 3., 7., 3., 6., 8., 6., 8., 8., 3., 8., 3., 8., 7., 8., 7., 8., 3.,
8., 3., 8., 7., 8., 7., 4., 4., 4., 4., 9., 7., 9., 7., 4., 4., 4., 4.,
9., 7., 9., 7., 7., 3., 7., 3., 6., 8., 6., 8., 7., 3., 7., 3., 6., 8.,
6., 8., 8., 3., 8., 3., 8., 7., 8., 7., 8., 3., 8., 3., 8., 7., 8., 7.},
{9, 4, 4});

auto tiled_3 = tile(data_2, {3, 2, 2});

auto expected_4 = array(
{4., 4., 4., 4., 9., 7., 9., 7., 4., 4., 4., 4., 9., 7., 9., 7., 7., 3.,
7., 3., 6., 8., 6., 8., 7., 3., 7., 3., 6., 8., 6., 8., 8., 3., 8., 3.,
8., 7., 8., 7., 8., 3., 8., 3., 8., 7., 8., 7., 4., 4., 4., 4., 9., 7.,
9., 7., 4., 4., 4., 4., 9., 7., 9., 7., 7., 3., 7., 3., 6., 8., 6., 8.,
7., 3., 7., 3., 6., 8., 6., 8., 8., 3., 8., 3., 8., 7., 8., 7., 8., 3.,
8., 3., 8., 7., 8., 7., 4., 4., 4., 4., 9., 7., 9., 7., 4., 4., 4., 4.,
9., 7., 9., 7., 7., 3., 7., 3., 6., 8., 6., 8., 7., 3., 7., 3., 6., 8.,
6., 8., 8., 3., 8., 3., 8., 7., 8., 7., 8., 3., 8., 3., 8., 7., 8., 7.,
4., 4., 4., 4., 9., 7., 9., 7., 4., 4., 4., 4., 9., 7., 9., 7., 7., 3.,
7., 3., 6., 8., 6., 8., 7., 3., 7., 3., 6., 8., 6., 8., 8., 3., 8., 3.,
8., 7., 8., 7., 8., 3., 8., 3., 8., 7., 8., 7., 4., 4., 4., 4., 9., 7.,
9., 7., 4., 4., 4., 4., 9., 7., 9., 7., 7., 3., 7., 3., 6., 8., 6., 8.,
7., 3., 7., 3., 6., 8., 6., 8., 8., 3., 8., 3., 8., 7., 8., 7., 8., 3.,
8., 3., 8., 7., 8., 7., 4., 4., 4., 4., 9., 7., 9., 7., 4., 4., 4., 4.,
9., 7., 9., 7., 7., 3., 7., 3., 6., 8., 6., 8., 7., 3., 7., 3., 6., 8.,
6., 8., 8., 3., 8., 3., 8., 7., 8., 7., 8., 3., 8., 3., 8., 7., 8., 7.},
{3, 6, 4, 4});

auto tiled_4 = tile(data_2, {3, 2, 2, 2});

auto expected_5 =
array({4., 4., 9., 7., 7., 3., 6., 8., 8., 3., 8., 7.}, {3, 2, 2});
auto tiled_5 = tile(data_2, {1, 1, 1});

auto expected_6 =
array({4., 4., 9., 7., 7., 3., 6., 8., 8., 3., 8., 7.}, {1, 3, 2, 2});
auto tiled_6 = tile(data_2, {1, 1, 1, 1});

CHECK(array_equal(expected_1, tiled_1).item<bool>());
CHECK(array_equal(expected_2, tiled_2).item<bool>());
CHECK(array_equal({expected_3}, {tiled_3}).item<bool>());
CHECK(array_equal({expected_4}, {tiled_4}).item<bool>());
CHECK(array_equal({expected_5}, {tiled_5}).item<bool>());
CHECK(array_equal({expected_6}, {tiled_6}).item<bool>());

CHECK_THROWS_AS(tile(data_2, {-1, 2}), std::invalid_argument);
}
TEST_CASE("tensordot") {
auto x = reshape(arange(60.), {3, 4, 5});
auto y = reshape(arange(24.), {4, 3, 2});
Expand Down Expand Up @@ -2314,4 +2388,4 @@ TEST_CASE("tensordot") {
65370.},
{3, 6});
CHECK(array_equal(z, expected).item<bool>());
}
}