micropython-ulab
micropython-ulab copied to clipboard
[FEATURE REQUEST] *Optional* support for dtypes uint32 and int32
Describe the solution you'd like
Optional support for dtypes uint32 and int32, similar to how complex number are optionally enabled by setting ULAB_SUPPORTS_COMPLEX.
Additional context
I skimmed a bit through #341 and https://github.com/v923z/micropython-ulab/issues/306#issuecomment-774664893. Sounds like the main concern is that adding more dtypes results in ulab's compiled size increasing dramatically (double?). Totally understand and agree with that concern!
However, I'm working on an application where the entire compiled firmware size is multiple MB in size. It does not matter to me if ulab's compiled size doubles (eg. ~120kB to ~240kB), that's negligible for this application.
And the solution provided by #342 does not work for my application. I unfortunately can't go into detail about my exact application right now (that will change soon!), but in short, I have an array where each element is a counter. The count values can easily exceed the capacity of 16-bit integers. Using a float array is not acceptable, because if the count value exceeds 23-bits (32-bit float mantissa size), then counts can start to be lost. Precision matters here, so lost counts from using floats are not acceptable.
Although this feature request is primarily motivated for my application, it would certainly be helpful for others as well, given the discussions in #341, #364, #306, etc.
I'm not quite sure how to go about it. If you have 5 base types, plus two optional, that gives you 4 possible combinations of configuration to keep track of. It's highly non-trivial. The case of the 32-bit integers is actually more involving than that of the complexes, because complexes are treated as two interleaved floating point arrays, while you might want to use integers for indexing, which means that all places, where indexing pops up in some way have also to be handled.
You might have seen that the complex implementation is not complete: in quite a few instances we decided that people wouldn't probably need a particular function with complex arguments, so we simply bail out.
You're a bit cryptic about what you actually want to do, but I'm wondering, whether there would be a simpler way.
You're a bit cryptic about what you actually want to do
Yeah, sorry. Wish I could say more, but I can't publicly reveal what I'm working on at the moment. That will change in the next few months once it goes live, but can't reveal too much publicly until then. And it's okay if this feature request gets put on hold until then, because it's only relevant to a small and non-critical part of the application.
If you have 5 base types, plus two optional, that gives you 4 possible combinations of configuration to keep track of.
IMO int32 and uint32 should both be enabled and disabled together, maybe with a macro called ULAB_SUPPORTS_INT32 or something. So that's only 2 combinations: with and without 32-bit integers.
I'm not quite sure how to go about it.
I'll admit, I'm not very familiar with the inner workings of ulab, so I probably don't fully appreciate all the challenges involved to implement this. But there appear to be a lot of instances in the codebase like this:
https://github.com/v923z/micropython-ulab/blob/88ef8935406f5a297dde66d74f2edd5d19bd6e7f/code/ndarray_operators.c#L59-L69
where it should be fairly straightforward (albeit, time consuming) to add something like the following, right?
#ifdef ULAB_SUPPORTS_INT32
else if(rhs->dtype == NDARRAY_UINT16) {
EQUALITY_LOOP(results, array, uint8_t, uint32_t, larray, lstrides, rarray, rstrides, ==);
}else if(rhs->dtype == NDARRAY_INT16) {
EQUALITY_LOOP(results, array, uint8_t, int32_t, larray, lstrides, rarray, rstrides, ==);
}
#endif
You're a bit cryptic about what you actually want to do
Yeah, sorry. Wish I could say more, but I can't publicly reveal what I'm working on at the moment.
I can't really help, if I don't know more. It might be that you need 32-bit integers only in very limited circumstances. Implementing that might be easier than just introducing a new type everywhere. There is the utils module, which we used in the past to add features with very limited scope. That could also be an option, I just don't know.
That will change in the next few months once it goes live, but can't reveal too much publicly until then. And it's okay if this feature request gets put on hold until then, because it's only relevant to a small and non-critical part of the application.
If you don't care about space and the like, can't you use doubles? That would give you 52 bits for storing integers.
If you have 5 base types, plus two optional, that gives you 4 possible combinations of configuration to keep track of.
IMO int32 and uint32 should both be enabled and disabled together, maybe with a macro called
ULAB_SUPPORTS_INT32or something. So that's only 2 combinations: with and without 32-bit integers.
Well, here you go:
- only base types
- base types + uint32 + int32
- base types + complex
- base types + complex + uint32 + int32
This makes four, even if int32 and uint32 are treated together.
I'm not quite sure how to go about it.
I'll admit, I'm not very familiar with the inner workings of ulab, so I probably don't fully appreciate all the challenges involved to implement this. But there appear to be a lot of instances in the codebase like this:
micropython-ulab/code/ndarray_operators.c
Lines 59 to 69 in 88ef893 if(rhs->dtype == NDARRAY_UINT8) { EQUALITY_LOOP(results, array, uint8_t, uint8_t, larray, lstrides, rarray, rstrides, ==); } else if(rhs->dtype == NDARRAY_INT8) { EQUALITY_LOOP(results, array, uint8_t, int8_t, larray, lstrides, rarray, rstrides, ==); } else if(rhs->dtype == NDARRAY_UINT16) { EQUALITY_LOOP(results, array, uint8_t, uint16_t, larray, lstrides, rarray, rstrides, ==); } else if(rhs->dtype == NDARRAY_INT16) { EQUALITY_LOOP(results, array, uint8_t, int16_t, larray, lstrides, rarray, rstrides, ==); } else if(rhs->dtype == NDARRAY_FLOAT) { EQUALITY_LOOP(results, array, uint8_t, mp_float_t, larray, lstrides, rarray, rstrides, ==); }
where it should be fairly straightforward (albeit, time consuming) to add something like the following, right?
Yes, that in itself would be relatively straightforward. But we would have to think about promoting types, which is done like this now:
https://github.com/v923z/micropython-ulab/blob/88ef8935406f5a297dde66d74f2edd5d19bd6e7f/code/ndarray_operators.c#L26-L37
If you want to add int32, then the rules stipulating
// int8 + int16 => int16
// int8 + uint16 => uint16
// uint16 + int16 => float
would have to be overridden. The case of the complexes was simple, because adding a complex to anything results in a complex, so the promotion rules are quite linear.
Then there is the issue of what you do with sorting, and what you do with the maximum length of axes. At the moment, that's pinned at 65535, so that it is contained in a 16-bit unsigned integer. Limiting the types to 16-bit integers and floats ensures that even if you use arrays for indexing, you don't run over the length of the array.
These are all almost trivial problems, except for the fact that a lot of code has to be inserted at various places. The question becomes more difficult, if you demand that you be able to switch types with a pre-processor variable, and that's where I start to exercise a lot more caution. It's is not simply copying code.
Understood, thank you for the explanation!
Perhaps we should revisit this discussion once I can discuss the application publicly. Truthfully, the part of the application that needs 32 bit integers is fairly minor, and there are likely simpler workarounds that can be implemented if needed.
Regardless, I would still love if 32 bit integer support could be implemented at some point, as I'm sure others would like to have. Though I also totally understand that it's not trivial to implement, and doesn't necessarily benefit the majority of users.
Don't forget there's already array.array('i', [...]). And you could write some simple custom C extension code (eg in a native .mpy file, or as a user C module) that operated on such arrays to do what you need.
Good point! Though I unfortunately need multi-dimensional arrays.
Sorry for the cryptic and contrived explanations from before, I can finally reveal the true reason for this request - we ported OpenCV to MicroPython! https://github.com/sparkfun/micropython-opencv
Standard Python OpenCV uses NumPy for the image data structures, so our port uses ulab NumPy. There are some OpenCV functions that require 32-bit integers, which became a problem for us since ulab NumPy doesn't support them, and we were trying to use ndarray data structures for the underlying C++ OpenCV functions.
We eventually found a solution to let the underlying C++ OpenCV functions create 32-bit Mat objects, then convert them to 32-bit float ndarray objects. It works and is relatively simple, but this conversion is inefficient, and it creates an API difference from standard Python OpenCV since the returned ndarray objects are floats instead of integers. Hence, the desire for 32-bit integer dtypes.
Truthfully, the implementation we have now is probably good enough. The Mat objects are usually not huge, so converting to ndarray float objects isn't a major problem. And I think users can easily adapt to the API difference, it's fairly minor. But I wanted to at least follow up on this thread. While we'd really appreciate 32-bit integer support being added, this feature request can probably be categorized as "nice to have" rather than a strong need.