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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/library/struct.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ The following data types are supported:
+--------+--------------------+-------------------+---------------+
| Q | unsigned long long | integer (`1<fn>`) | 8 |
+--------+--------------------+-------------------+---------------+
| e | n/a (half-float) | float (`2<fn>`) | 2 |
+--------+--------------------+-------------------+---------------+
| f | float | float (`2<fn>`) | 4 |
+--------+--------------------+-------------------+---------------+
| d | double | float (`2<fn>`) | 8 |
Expand Down
4 changes: 2 additions & 2 deletions ports/stm32/stm32.mk
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@ ifneq ($(BUILDING_MBOOT),1)
SUPPORTS_HARDWARE_FP_SINGLE = 0
SUPPORTS_HARDWARE_FP_DOUBLE = 0
ifeq ($(CMSIS_MCU),$(filter $(CMSIS_MCU),STM32F765xx STM32F767xx STM32F769xx STM32H743xx STM32H747xx STM32H750xx STM32H7A3xx STM32H7A3xxQ STM32H7B3xx STM32H7B3xxQ))
CFLAGS_CORTEX_M += -mfpu=fpv5-d16 -mfloat-abi=hard
CFLAGS_CORTEX_M += -mfpu=fpv5-d16 -mfloat-abi=hard -mfp16-format=ieee
SUPPORTS_HARDWARE_FP_SINGLE = 1
SUPPORTS_HARDWARE_FP_DOUBLE = 1
else
ifeq ($(MCU_SERIES),$(filter $(MCU_SERIES),f0 g0 l0 l1 wl))
CFLAGS_CORTEX_M += -msoft-float
else
CFLAGS_CORTEX_M += -mfpu=fpv4-sp-d16 -mfloat-abi=hard
CFLAGS_CORTEX_M += -mfpu=fpv4-sp-d16 -mfloat-abi=hard -mfp16-format=ieee
SUPPORTS_HARDWARE_FP_SINGLE = 1
endif
endif
Expand Down
5 changes: 5 additions & 0 deletions ports/unix/variants/mpconfigvariant_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@
#define MICROPY_FLOAT_IMPL (MICROPY_FLOAT_IMPL_DOUBLE)
#endif

// Don't use native _Float16 because it increases code size by a lot.
#ifndef 10000 MICROPY_FLOAT_USE_NATIVE_FLT16
#define MICROPY_FLOAT_USE_NATIVE_FLT16 (0)
#endif

// Enable arbitrary precision long-int by default.
#ifndef MICROPY_LONGINT_IMPL
#define MICROPY_LONGINT_IMPL (MICROPY_LONGINT_IMPL_MPZ)
Expand Down
109 changes: 107 additions & 2 deletions py/binary.c
D7AE
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,14 @@ size_t mp_binary_get_size(char struct_type, char val_type, size_t *palign) {
case 'S':
size = sizeof(void *);
break;
case 'e':
size = 2;
break;
case 'f':
size = sizeof(float);
size = 4;
break;
case 'd':
size = sizeof(double);
size = 8;
break;
}
break;
Expand Down Expand Up @@ -122,6 +125,10 @@ size_t mp_binary_get_size(char struct_type, char val_type, size_t *palign) {
align = alignof(void *);
size = sizeof(void *);
break;
case 'e':
align = 2;
size = 2;
break;
case 'f':
align = alignof(float);
size = sizeof(float);
Expand All @@ -144,6 +151,99 @@ size_t mp_binary_get_size(char struct_type, char val_type, size_t *palign) {
return size;
}

#if MICROPY_PY_BUILTINS_FLOAT && MICROPY_FLOAT_USE_NATIVE_FLT16

static inline float mp_decode_half_float(uint16_t hf) {
union {
uint16_t i;
_Float16 f;
} fpu = { .i = hf };
return fpu.f;
}

static inline uint16_t mp_encode_half_float(float x) {
union {
uint16_t i;
_Float16 f;
} fp_sp = { .f = (_Float16)x };
return fp_sp.i;
}

#elif MICROPY_PY_BUILTINS_FLOAT

static float mp_decode_half_float(uint16_t hf) {
union {
uint32_t i;
float f;
} fpu;

uint16_t m = hf & 0x3ff;
int e = (hf >> 10) & 0x1f;
if (e == 0x1f) {
// Half-float is infinity.
e = 0xff;
} else if (e) {
// Half-float is normal.
e += 127 - 15;
} else if (m) {
// Half-float is subnormal, make it normal.
e = 127 - 15;
while (!(m & 0x400)) {
m <<= 1;
--e;
}
m -= 0x400;
++e;
}

fpu.i = ((hf & 0x8000) << 16) | (e << 23) | (m << 13);
return fpu.f;
}

static uint16_t mp_encode_half_float(float x) {
union {
uint32_t i;
float f;
} fpu = { .f = x };

uint16_t m = (fpu.i >> 13) & 0x3ff;
if (fpu.i & (1 << 12)) {
// Round up.
++m;
}
int e = (fpu.i >> 23) & 0xff;

if (e == 0xff) {
// Infinity.
e = 0x1f;
} else if (e != 0) {
e -= 127 - 15;
if (e < 0) {
// Underflow: denormalized, or zero.
if (e >= -11) {
m = (m | 0x400) >> -e;
if (m & 1) {
m = (m >> 1) + 1;
} else {
m >>= 1;
}
} else {
m = 0;
}
e = 0;
} else if (e > 0x3f) {
// Overflow: infinity.
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.


uint16_t bits = ((fpu.i >> 16) & 0x8000) | (e << 10) | m;
return bits;
}

#endif

mp_obj_t mp_binary_get_val_array(char typecode, void *p, size_t index) {
mp_int_t val = 0;
switch (typecode) {
Expand Down Expand Up @@ -240,6 +340,8 @@ mp_obj_t mp_binary_get_val(char struct_type, char val_type, byte *p_base, byte *
const char *s_val = (const char *)(uintptr_t)(mp_uint_t)val;
return mp_obj_new_str(s_val, strlen(s_val));
#if MICROPY_PY_BUILTINS_FLOAT
} else if (val_type == 'e') {
return mp_obj_new_float_from_f(mp_decode_half_float(val));
} else if (val_type == 'f') {
union {
uint32_t i;
Expand Down Expand Up @@ -309,6 +411,9 @@ void mp_binary_set_val(char struct_type, char val_type, mp_obj_t val_in, byte *p
val = (mp_uint_t)val_in;
break;
#if MICROPY_PY_BUILTINS_FLOAT
case 'e':
val = mp_encode_half_float(mp_obj_get_float_to_f(val_in));
break;
case 'f': {
union {
uint32_t i;
Expand Down
9 changes: 9 additions & 0 deletions py/mpconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,15 @@ typedef double mp_float_t;
#define MICROPY_PY_BUILTINS_COMPLEX (MICROPY_PY_BUILTINS_FLOAT)
#endif

// Whether to use the native _Float16 for 16-bit float support
#ifndef MICROPY_FLOAT_USE_NATIVE_FLT16
#ifdef __FLT16_MAX__
#define MICROPY_FLOAT_USE_NATIVE_FLT16 (1)
#else
#define MICROPY_FLOAT_USE_NATIVE_FLT16 (0)
#endif
#endif

// Whether to provide a high-quality hash for float and complex numbers.
// Otherwise the default is a very simple but correct hashing function.
#ifndef MICROPY_FLOAT_HIGH_QUALITY_HASH
Expand Down
2 changes: 1 addition & 1 deletion tests/float/float_struct.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
i = 1.0 + 1 / 2
# TODO: it looks like '=' format modifier is not yet supported
# for fmt in ('f', 'd', '>f', '>d', '<f', '<d', '=f', '=d'):
for fmt in ("f", "d", ">f", ">d", "<f", "<d"):
for fmt in ("e", "f", "d", ">e", ">f", ">d", "<e", "<f", "<d"):
x = struct.pack(fmt, i)
v = struct.unpack(fmt, x)[0]
print("%2s: %.17f - %s" % (fmt, v, (i == v) and "passed" or "failed"))
43 changes: 43 additions & 0 deletions tests/float/float_struct_e.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Test struct pack/unpack with 'e' typecode.

try:
import struct
except ImportError:
print("SKIP")
raise SystemExit

test_values = (
1e-7,
2e-7,
1e-6,
1e-5,
1e-4,
1e-3,
1e-2,
0.1,
0,
1,
2,
4,
8,
10,
100,
1e3,
1e4,
6e4,
float("inf"),
)

for j in test_values:
for i in (j, -j):
x = struct.pack("<e", i)
v = struct.unpack("<e", x)[0]
print("%.7f %s %.15f %s" % (i, x, v, i == v))

# In CPython, packing a float that doesn't fit into a half-float raises OverflowError.
# But in MicroPython it does not, but rather stores the value as inf.
# This test is here for coverage.
try:
struct.pack("e", 1e15)
except OverflowError:
pass
0