8000 struct: Implement 'e' half-float format by smurfix · Pull Request #12799 · micropython/micropython · GitHub
[go: up one dir, main page]

Skip to content

struct: Implement 'e' half-float format #12799

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 20, 2024
Merged

Conversation

smurfix
Copy link
Contributor
@smurfix smurfix commented Oct 25, 2023

py/binary: struct: Support half-float.

This change implements the 'e' half-float format:
10-bit mantissa, 5-bit exponent.

The first patch uses "real" math to assemble and disassemble the half-float bits, while the second replaces the pesky math with mangling the half-float bits to real-float bits. This saves ~600 bytes of code size.

Closes #7467.

@smurfix smurfix changed the title Pack e struct: Implement 'e' half-float format Oct 25, 2023
@github-actions
Copy link
github-actions bot commented Oct 25, 2023

Code size report:

   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:  +288 +0.035% standard
      stm32:   -88 -0.022% PYBV10
     mimxrt:   +72 +0.020% TEENSY40
        rp2:  +168 +0.050% RPI_PICO
       samd:   +72 +0.027% ADAFRUIT_ITSYBITSY_M4_EXPRESS

@codecov
Copy link
codecov bot commented Oct 25, 2023

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 98.39%. Comparing base (77f08b7) to head (9d27183).

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #12799   +/-   ##
=======================================
  Coverage   98.39%   98.39%           
=======================================
  Files         161      161           
  Lines       21156    21199   +43     
=======================================
+ Hits        20816    20859   +43     
  Misses        340      340           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@dlech
Copy link
Contributor
dlech commented Oct 25, 2023

This is extending a standard Python module with non-standard features. It would be better to stick with the standard functionality for maximum compatibility (or a subset of it to keep the code size down).

As a workaround, it seems like you could use the H format to get the 16-bit value as a small int then make function in a different library to do the bitwise conversion of the int object to a regular float object or a half-float object if the platform supports it.

Or this might be a job for uctypes instead of struct.

@dpgeorge
Copy link
Member

This is extending a standard Python module with non-standard features.

I'm not sure what you mean, Python did add support for "e" in struct in version 3.6.


@smurfix thanks for the implementation. Did you find a use/need for this? I'd be interested to know.

@dlech
Copy link
Contributor
dlech commented Oct 26, 2023

I'm not sure what you mean, Python did add support for "e" in struct in version 3.6.

🤦 how did I never notice that before

@smurfix
Copy link
Contributor Author
smurfix commented Oct 26, 2023

Yep, I need this for CBOR compatibility. The CPython CBOR2 package does a little "if it fits in 16 bits without losing accuracy then use two bytes, not four" dance that can't be turned off (and why should it?); many for other languages also do this.

I'll probably add a nice and small CBOR library (nothing like CBOR2 obviously, that's huge by now) to micropython-lib next.

@dmazzella
Copy link
Contributor

@smurfix if you need a small cbor try my library, I have implemented support for half-float as well ucbor

py/binary.c Outdated
@@ -175,6 +241,8 @@ mp_obj_t mp_binary_get_val_array(char typecode, void *p, size_t index) {
return mp_obj_new_int_from_ull(((unsigned long long *)p)[index]);
#endif
#if MICROPY_PY_BUILTINS_FLOAT
case 'e':
return mp_obj_new_float_from_f(mp_decode_half(((uint16_t *)p)[index]));
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this line is needed, it would be used for array.array("e").

Copy link
Member

Choose a reason for hiding this comment

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

I removed this, it's not needed.

@dpgeorge dpgeorge added the py-core Relates to py/ directory in source label Oct 27, 2023
e = 0x1F;
m = 0;
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Please add test cases to test all paths of this function. I think it's fine to do this in the existing float_struct.py test, just loop over a handful of values to pack/unpack.

Copy link
Member

Choose a reason for hiding this comment

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

I added comprehensive tests for these functions that cover all code paths.

@jimmo
Copy link
Member
jimmo commented Oct 27, 2023

@smurfix Which architectures do you need to support this on?

On our arm targets, if you enable -fp16-format=ieee then you get the _Float16 type. On Cortex-M4 (with floating point, e.g. stm32f4) the conversion becomes https://developer.arm.com/documentation/100076/0200/a32-t32-instruction-set-reference/floating-point-instructions--32-bit-/vcvtb--vcvtt--half-precision-extension-
Tested on pybv11, works well.

On Cortex-M0 this is implemented by libgcc: https://github.com/gcc-mirror/gcc/blob/master/libgcc/config/arm/fp16.c#L64 but this appears to generate larger code than your version.

On x86 you can also use _Float16 on gcc/clang.

There is no support on xtensa (even in libgcc).

So, would it be acceptable to only add support for e on platforms where hardware support is available (or where code size isn't such a factor)? (i.e. arm/thumb with VFPv3+hf or VFPv4, aarch64, x86, x64)

@smurfix
Copy link
Contributor Author
smurfix commented Oct 27, 2023

There is no support on xtensa (even in libgcc).

Well, given that xtensa is one of the platforms I need to run my code on …

@dpgeorge
Copy link
Member

I think this is a useful addition, the e typecode.

But it would be good to support hardware 16-bit floats and the _Float16 type when it's available.

@smurfix are you able and willing to update this PR to work with _Float16 when possible, and fallback to your built-in routines when not?

@smurfix
Copy link
Contributor Author
smurfix commented Jan 25, 2024

Done; assuming that the existence of the FLT16_MAX macro mirrors the existence of _Float16, which may or may not be correct …

@AmirHmZz
Copy link
Contributor
AmirHmZz commented Feb 2, 2024

Did you find a use/need for this? I'd be interested to know.

@dpgeorge Another use case for half-float is dealing with bh1730fvi ambient light sensor which uses half-float.

@dpgeorge dpgeorge added this to the release-1.23.0 milestone Feb 3, 2024
py/binary.c Outdated
return mp_obj_new_float_from_f(fpu.f);
#else
return mp_obj_new_float_from_f(mp_decode_half(val));
#endif
Copy link
Member

Choose a reason for hiding this comment

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

Instead of putting the "does target have float16 support" in these locations, I suggest having this line as simply:

return mp_obj_new_float_from_f(mp_decode_half(val));

for both cases, and then logic elsewhere to provide the correct implementation of mp_decode_half(). Eg:

#ifdef FLT16_MAX
static inline float mp_decode_half(...) {
    union { ... }
    return fpu.f;
}
#else
...
#endif

This will fix a few build errors the current implementation has when float16 is available.

Copy link
Member

Choose a reason for hiding this comment

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

I applied the above suggestion.

@dpgeorge
Copy link
Member

assuming that the existence of the FLT16_MAX macro mirrors the existence of _Float16, which may or may not be correct

That doesn't look correct for ARM targets. If I enable -mfp16-format=ieee on arm-non-eabi-gcc then the following macros are defined:

$ echo | arm-none-eabi-gcc -mfp16-format=ieee -dM -E - | grep FLT16                                                             
#define __FLT16_HAS_QUIET_NAN__ 1
#define __FLT16_MIN_EXP__ (-13)
#define __FLT16_MAX_10_EXP__ 4
#define __FLT16_DECIMAL_DIG__ 5
#define __FLT16_DENORM_MIN__ 5.9604644775390625e-8F16
#define __FLT16_MIN_10_EXP__ (-4)
#define __FLT16_DIG__ 3
#define __FLT16_IS_IEC_60559__ 1
#define __FLT16_MAX_EXP__ 16
#define __FLT16_EPSILON__ 9.7656250000000000e-4F16
#define __FLT16_NORM_MAX__ 6.5504000000000000e+4F16
#define __FLT16_MAX__ 6.5504000000000000e+4F16
#define __FLT16_HAS_INFINITY__ 1
#define __FLT16_MANT_DIG__ 11
#define __FLT16_MIN__ 6.1035156250000000e-5F16
#define __FLT16_HAS_DENORM__ 1

So I suggest to use __FLT16_MAX__ to detect the built-in type.

@dpgeorge dpgeorge force-pushed the pack_e branch 2 times, most recently from 2a701e7 to c11f827 Compare March 20, 2024 02:52
smurfix and others added 4 commits March 20, 2024 14:13
This commit implements the 'e' half-float format: 10-bit mantissa, 5-bit
exponent.  It uses native _Float16 if supported by the compiler, otherwise
uses custom bitshifting encoding/decoding routines.

Signed-off-by: Matthias Urlichs <matthias@urlichs.de>
Signed-off-by: Damien George <damien@micropython.org>
Signed-off-by: Damien George <damien@micropython.org>
Using it increases code size by about 2k.

Signed-off-by: Damien George <damien@micropython.org>
Signed-off-by: Damien George <damien@micropython.org>
@dpgeorge
Copy link
Member

I have rebased this on latest master and made the following changes:

  • Used __FLT16_MAX__ to detect if the compiler supports _Float16.
  • Added option MICROPY_FLOAT_USE_NATIVE_FLT16 so a port can have fine control over using native _Float16 or the custom conversion functions added in this PR.
  • Fixed decoding and encoding of subnormals (encoding was out by a factor of 2, and decoding just didn't work at all).
  • Implemented rounding in encoding.
  • Added more tests.

9E88 @dpgeorge dpgeorge merged commit 9d27183 into micropython:master Mar 20, 2024
@smurfix
Copy link
Contributor Author
smurfix commented Mar 20, 2024

Thank you for your work on this!

@AmirHmZz
Copy link
Contributor

@dpgeorge @smurfix Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
py-core Relates to py/ directory in source
Projects
None yet
Development

Successfully merging this pull request may close these issues.

struct module: Support for half-precision floats ("e")
6 participants
0