Skip to content

interop: explicit ±inf recode shim for copying between cfloat and IEEE binary16 / OCP E5M2 #69

Description

@Ravenwater

Follow-up from #57, which closed the "make cfloat match IEEE" idea as wontfix.
cfloat is the encoding this package implements and zero-copy aliasing with IEEE
binary16 stays unsupported. What is still worth having is an explicit, cheap
recode
so crossing that boundary by copy is correct by construction, instead
of relying on the accident that finite data aliases cleanly.

The transform is tiny and exact

The two encodings differ in exactly two patterns per sign, and the mapping
between them is a straight swap:

fp16 ↔ IEEE binary16 fp8e5m2 ↔ OCP E5M2
positive 0x7c00 ↔ 0x7ffe 0x7c ↔ 0x7e
negative 0xfc00 ↔ 0xfffe 0xfc ↔ 0xfe

Everything else is byte-identical. Both formats have exactly one infinity and the
same number of NaN encodings per sign, so this is a bijection.

Verified by applying the swap across the entire encoding space and comparing
decoded values against the reference:

fp16    <-> IEEE binary16 : 0 mismatches / 65536 , self-inverse = True
fp8e5m2 <-> OCP E5M2      : 0 mismatches / 256   , self-inverse = True

Two properties fall out that make this pleasant to ship:

  • Self-inverse. One function serves both directions, and a round trip is
    bit-identity. No separate to_/from_ implementations to keep in step.
  • NaN payloads survive, because every pattern except the four is untouched.

Why not just use astype

astype is correct in the value domain and stays the recommended path for
arrays. The shim is for the case where you hold raw bytes known to be IEEE
binary16 — a file, a socket, an mmap, another framework's buffer — and would
otherwise reach for np.frombuffer(buf, dtype=ud.fp16), which is exactly the
unsupported aliasing that silently corrupts ±inf.

It is also substantially cheaper, because it is a byte copy plus four equality
tests rather than a decode/encode round trip through double. On 5M elements:

astype path : 93.4 ms
recode path : 15.2 ms   (6.2x faster)

And it can be written in pure Python/NumPy — no C++, no rebuild:

b = np.frombuffer(raw, dtype=np.uint16).copy()
for x, y in ((0x7c00, 0x7ffe), (0xfc00, 0xfffe)):
    i, j = b == x, b == y
    b[i], b[j] = y, x
arr = np.frombuffer(b.tobytes(), dtype=ud.fp16)

To be explicit about what this is not

It is a copy, deliberately. #57 settled that zero-copy aliasing between these
types will not be supported, and this does not reopen that. The value is naming
the boundary crossing so it appears in the code, and making it cheap enough that
nobody is tempted to alias instead.

Sketch

Something like a small universal_dtypes.interop module:

recode_ieee(arr_or_buffer, dtype=ud.fp16)   # IEEE bits -> fp16 array
recode_ieee(fp16_array)                     # fp16 -> IEEE bits (same function)

Open questions worth settling when this is picked up:

  • Whether it takes/returns buffers, arrays, or both.
  • Whether it should refuse dtypes with no IEEE counterpart loudly, rather than
    silently no-op'ing.
  • Whether fp8e5m2 ↔ OCP E5M2 ships at the same time (the transform is already
    verified above) or whether fp16 alone is enough to start.
  • Whether to expose the pattern table so downstream code can do the same recode
    on data we never see.

Note

While measuring the astype baseline for this issue, astype turned out to
convert some IEEE NaN payloads into infinity — a separate, live correctness
bug with an upstream root cause in cfloat, unrelated to the shim. The recode
proposed here handles those patterns correctly, but that is not a reason to use
the shim as a workaround; the bug should be fixed on its own terms. Filed
separately.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

dtypeNumPy dtype implementationenhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions