Conversation
louen
force-pushed
the
val/add-unique-op
branch
from
September 14, 2026 17:44
a21d606 to
2c70d1b
Compare
louen
marked this pull request as ready for review
September 14, 2026 18:17
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_nanandreturn_indexoptional parameters found in jax (et al.) are not supported yet.jvpis not supported (unlike Jax). This is a limitation of current mlx'sscatter.AI usage disclosure: