Add fixed size unique operation - #4501
Conversation
a21d606 to
2c70d1b
Compare
| (2, [1.0, 1.0, 2.0], [1.0, 0.0, 1.0]), | ||
| ): | ||
| with self.subTest(size=size, data=data): | ||
| grad = mx.grad(lambda x: mx.sum(mx.unique(x, size)))(mx.array(data)) |
There was a problem hiding this comment.
We should have the tests here include the variations in mx.unique - the case with return_inverse=True, and fill_value specified
There was a problem hiding this comment.
do you mean specifically for gradient computation or are you asking more generally for these parameters to be exercised more in the test ?
|
Adding One place I wasn't sure is when using a fill value that cannot be represented with the current type (e.g. using |
|
Now supports |
This PR adds a
uniqueoperator, as a convenience to match with similar operations found in numpy, torch and jax.The operation is implemented as a composite of other primitive ops (
sortandscatter) and is mostly useful as a parity convenience operation.Size-dependency
One of the main challenges of a
uniqueoperation in MLX is that the size of the unique array is unknown before evaluation, which doesn't really fit MLX's design (see discussion in e.g. #246, #568 and #927, decision in @awni's gist)This issue is also a problem for jax and its jit compiler ; and this PR follows their approach to resolving this issue: a user-passed
sizeparameter which determines the size of the output array independently from the input.In the case of jax they propose the size-independent version as an option. The MLX implementation in this PR makes this a requirement.
Behaviours and edge-cases
sizeThe new function aims for parity of behaviour with the jax version (with the provided
size) wherever possible.The choice of
sizewill determine what happens to the output array:sizeis smaller than the number of unique elements, the output will be truncatedsizeis larger than the number of unique elements, the output will be padded withfill_valuefill_valueis not provided, the first element of the sorted array is used (this is usually the min value of the array, except in pathological cases).An empty array is generally a valid input, but
unique([], size)will throw ifsizeis greater than 0 (since there is no min value to fill the result with). However,unique([], size, fill_value=-1)will return[-1] * size.A
sizeof 0 is supported, returning an empty array.Optional return arrays
uniqueoptionally returns thecountsof each unique element and theinverseindices if requested.If the unique array is padded, the counts for the padding elements are 0. This means that
count.sum() == a.size()if and only if the output was not truncated. In the truncated case, the counts for the unique elements are truncated too, socount.sum()will be smaller.The inverse array holds indices in unique that allow to reconstruct the original array. If the output is truncated, those indices are clamped at
size-1, so thata = unique[inverse]should always be valid ifsize > 0, but will only reconstruct the original array if the unique array was not truncated. Otherwise, the bigger elements will be replaced by the largest kept value.Note that
inverse's dtype isuint32(matching other mlx ops returning indices such asargmax) where jax usesint32.Differentiation
Gradient propagates up the
uniquearray back toa, deterministically through the first appearing representative of each of the unique array elements.NaN handling
NaNvalues are handled and placed at the end of the sorted unique array (before padding); but consistently withNaN != NaNthey do not collapse into one single unique value.This is a significant disparity with jax (which collapses NaNs by default, but allows to opt out).
We could support NaN collapse at the cost of an extra argument and an extra check on the edge detection to match with jax's behaviour.
Limitations
boolarrays are unsupported on metal (this is due to the fact thatsortfails on bool arrays). Unique on a bool array is only guaranteed to work on CPU stream, at least on Metal platforms.argsortdiverges between cpu and gpu on complexes with NaN #4502)axis,equal_nanandoptional parameters found in jax (et al.) are not supported yet.return_indexjvpis not supported (unlike Jax). This is a limitation of current mlx'sscatter.AI usage disclosure: