Make array conform to the Python Buffer Protocol - #323
Conversation
|
This looks super cool! One thing I'm wondering is what is the expected behavior when the |
|
@awni thanks for the feedback. I'll be happy to contribute if this is a desired feature. Regarding your question:
I think this is handled by Maybe we could take this further by constructing a numpy array via the memory view and return it form This should work for all but bfloat16, where we could still return a converted readonly float32 np array. |
|
Even better: @awni I updated this draft and I think it is quite a simplification and generalization compared to |
b79bc48 to
8e8cfb9
Compare
|
This looks great to me. I don't see any obvious reasons not to go this path. It seems strictly more useful than Adding @jagrit06 @angeloskath in case they see anything I might be missing here. |
|
@dastrobu I think you should PR this! |
|
Let me check some details and add some test cases, then I'll turn the draft into a PR for review. |
9ab1e18 to
62984bc
Compare
62984bc to
b1441d1
Compare
|
PR is ready for review. Looking forward to your feedback.
@awni I added two test cases to verify references are handled as expected. |
|
This looks great to me. I am trying to remember why we opted not to implement the buffer protocol. I can't think of any reason. However, we were returning non-writeable arrays so that it doesn't cause confusing behavior. For instance the example provided (thanks for writing a nice document btw) is not quite right. The reason it returns 0 is because you are making a new array. If instead you were simply returning |
That code is a pretty explicit situation ( I'm not advocating for writeable buffers, just that case doesn't have much weight for me. What exactly do we lose by making buffers read only (the safer and hence default option)?
|
Thank you for examining the example. Upon reflection, it appears to be unrelated to readonly views, as demonstrated by: This is doable with the current implementation, and of course it would also not get the right gradients. Nevertheless, unintended conversions may occur when interfacing with certain libraries. It seems advisable to highlight this behavior to developers in any case. So this doesn't appear to be inherently advantageous or detrimental to writable buffers.
This was actually what started me to look into this. If buffers are exposed read only, mx.data should brute force write to them anyway. Pipelines are currently implemented such that buffers are modified in place. So if buffers should stay read only this would require some refactoring. I am not sure what performance impact that would have on pipelines operating on large datasets. Once there is agreement on the direction to go, I can update the docs accordingly. |
I thought about this a bit more, and I appreciate this use case (and similar use cases), but I don't think they are worth making the buffer writeable for. Putting MLX writeable array buffers into black box APIs seems risky in general. Shall we go with read-only for now? Is everyone ok with that? @angeloskath @dastrobu ? |
Could you elaborate on your specific concerns? Furthermore, it would be valuable to discuss the recommended approach for mlx-data or similar use cases. Take, for instance, the I am open to the read-only solution, provided we can establish a clear and viable path for addressing such cases. |
|
@dastrobu we discussed a bit offline and the conclusion was that we can keep buffers writable (as you have it). We'll see how it goes, if there are some hiccups from we can come back and change it. Did you have anything else to add to this PR? If not I can take a final pass and we can land it? |
angeloskath
left a comment
There was a problem hiding this comment.
Generally looks great! The docs need updating but other than that I think it ready to merge.
|
|
||
| a = mx.arange(3) | ||
| b = np.array(a) | ||
| c = mx.array(b) |
There was a problem hiding this comment.
This will always be a copy, perhaps it could be mentioned.
There was a problem hiding this comment.
it is mentioned below in "By default, NumPy copies data to a new array." but I also added an in place comment for clarification.
| def g(x): | ||
| x_view = np.array(x, copy=False) | ||
| x_view[:] += 1 # modify memory without telling mx | ||
| return mx.array(x @ x) |
There was a problem hiding this comment.
I would remove the pure mlx example above and change this example to
x_view[:] *= x_view
return x.sum()with x being 2s or sth to clearly show that even though the output is x**2 the gradient will be 1s.
There was a problem hiding this comment.
Good idea. I was thinking of this example in the beginning but did not know how to express it properly.
I update the example and the explanation below. Please review this section again.
There was a problem hiding this comment.
I don't think this should be in the examples. Perhaps in Usage?
There was a problem hiding this comment.
👍
Moved all the usage files into usage/ to have the same folder structure as for other main sections.
27ec1ac to
43a2506
Compare
Great approach. I'll be the first one to create an issue on a hiccup if I find one 😉
Regarding the PR, I've incorporated all our discussions and considered the comments from @angeloskath in updating the documentation. Please take a moment to review the revised documentation. Beyond that, the PR is good to go from my perspective. Lastly, a big thank you to all the reviewers for engaging in such a positive and constructive discussion. In my opinion, the success of open source projects is not solely dependent on good code but also on individuals like you who invest time and effort in discussions with community members like myself. |
43a2506 to
1b206a5
Compare
awni
left a comment
There was a problem hiding this comment.
This is so cool, thanks! I also really appreciate the nice work on the docs, that's well done! I left a couple of minor comments there could you take a look?
O/w good to go here.
The method __array__` is replaced by implementing the Python Buffer Protocol.
1b206a5 to
84bf83e
Compare
|
|
||
| JAX | ||
| --- | ||
| JAX fully supports the buffer protocol. |
The method __array__` is replaced by implementing the Python Buffer Protocol.
Thanks, everything should be resolved now. |
Proposed changes
Make array conform to the Buffer Protocol.
The method
__array__is replaced by implementing the Python Buffer Protocol.Summary:
__array__is removed.np.array(mx.ones(1, dtype=mx.bfloat16))will fail, butnp.array(mx.ones(1, dtype=mx.bfloat16).astype(mx.float32))will work.mlx-datamay use mx arrays instead of numpy arrays, as memory views are writable. See mlx-data issue #20.test_buffer_protocol_tf.Closes #320
Writable Memory Views
Enabling writable memory views has both advantages and a drawback.
As highlighted in the summary,
mlx-datarelies on in-place modifications to buffers. Ifmlx-datawere to usemlxarrays in the future, which is a sensible idea,mlxarrays must expose writable buffers, as demonstrated in this pull request. An alternative would be to refactormlx-datasuch that it always returns new arrays on transformations and does not modify memory in place.The downside is that direct modification of memory through buffers will not be captured by the grad tracer. Thus, this pull request allows developers to unintentionally cause issues, as illustrated in the following test case:
If preventing such issues is desired, the pull request can be modified to return read-only buffers easily, albeit with the drawback of not supporting in-place operations as done in
mlx-data. For comparison, TensorFlow has implemented the buffer protocol such that it returns read-only buffers.From my perspective, having the flexibility to perform in-place operations on buffers outweighs the concern of potential broken gradients. I would like reviewers to pay special attention to this aspect.
Checklist
Put an
xin the boxes that apply.pre-commit run --all-filesto format my code / installed pre-commit prior to committing changes