Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8c7d5c3
feat(python): add bfloat16 and bfloat16_array support
asadjan4611 Feb 12, 2026
c89d86e
style(python): fix code formatting for bfloat16 implementation
asadjan4611 Feb 12, 2026
b2bb7c6
fix(python): remove trailing newline in bfloat16_array.py to match co…
asadjan4611 Feb 12, 2026
6537f74
fix(python): resolve Cython compilation errors for bfloat16
asadjan4611 Feb 12, 2026
b6793a5
fix(python): use memcpy for safe type punning in bfloat16 conversion
asadjan4611 Feb 13, 2026
e387339
fix(python): use explicit size constant for ARM compatibility
asadjan4611 Feb 13, 2026
71241ba
fix(python): correct row schema Arrow conversion type ids
asadjan4611 Feb 13, 2026
8066ace
fix(python): build and export bfloat16 python module
asadjan4611 Feb 13, 2026
8ed6f57
docs(python): document bfloat16 support and stabilize pure mode seria…
asadjan4611 Feb 13, 2026
9fcb74d
fix(python): configure bazel shell for windows editable builds
asadjan4611 Feb 13, 2026
8866639
Merge branch 'main' into feat/python-bfloat16-support
asadjan4611 Feb 20, 2026
102acfb
fix(python): restore xlang serializer base after conflict merge
asadjan4611 Feb 20, 2026
bedb82e
fix(python): restore xlang serializer base in cython runtime
asadjan4611 Feb 21, 2026
2f2b6b4
fix(python): align bfloat16 serializers with unified API and typed bu…
asadjan4611 Feb 21, 2026
c9e2e12
fix(python): resolve bfloat16 cython build and serializer API drift
asadjan4611 Feb 21, 2026
56f45d3
style(python): align bfloat16 files with ci formatter
asadjan4611 Feb 21, 2026
efb0100
fix(python): include bfloat16 pxd in format cython target
asadjan4611 Feb 21, 2026
f1a3f42
fix(python): resolve bfloat16 cython redeclaration and static method …
asadjan4611 Feb 21, 2026
9d74ae2
fix(python): remove obsolete xlang arg for dataclass serializers
asadjan4611 Feb 21, 2026
ec59341
fix(python): accept legacy xlang kwarg in dataclass serializers
asadjan4611 Feb 21, 2026
848a1cd
fix(python): repair unsigned xlang dataclass refs and retry bazel fetch
asadjan4611 Feb 21, 2026
66dbf19
style(python): format setup retry log line
asadjan4611 Feb 21, 2026
3ac6ba8
fix(python): remove keyword arguments from cpdef function calls in co…
asadjan4611 Feb 21, 2026
308bde9
fix(python): remove keyword arguments from cpdef function calls in st…
asadjan4611 Feb 21, 2026
e3a1e63
fix(python): preserve bfloat16 NaN semantics, fix hash contract, expa…
asadjan4611 Mar 12, 2026
d5fa636
Merge apache/main into feat/python-bfloat16-support
asadjan4611 Mar 12, 2026
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
28 changes: 28 additions & 0 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,34 @@ fory.register(Person.class, "example.Person");
Person person = (Person) fory.deserialize(binaryData);
```

### BFloat16 Support

`pyfory` supports `bfloat16` scalar values and `bfloat16` arrays in xlang mode:

- Scalar type: `pyfory.BFloat16` (type id `18`)
- Array type: `pyfory.BFloat16Array` (type id `54`)

```python
import pyfory
from pyfory import BFloat16, BFloat16Array

fory = pyfory.Fory(xlang=True, ref=False, strict=True)

# Scalar bfloat16
v = BFloat16(3.1415926)
data = fory.serialize(v)
out = fory.deserialize(data)
print(float(out))

# bfloat16 array
arr = BFloat16Array([1.0, 2.5, -3.25])
data = fory.serialize(arr)
out = fory.deserialize(data)
print(out)
```

`BFloat16Array` stores values in a packed `array('H')` representation and writes bytes in little-endian order for cross-language compatibility.

## 📊 Row Format - Zero-Copy Processing

Apache Fury™ provides a random-access row format that enables reading nested fields from binary data without full deserialization. This drastically reduces overhead when working with large objects where only partial data access is needed. The format also supports memory-mapped files for ultra-low memory footprint.
Expand Down
15 changes: 15 additions & 0 deletions python/pyfory/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
TaggedUint64Serializer,
Float32Serializer,
Float64Serializer,
BFloat16Serializer,
StringSerializer,
DateSerializer,
TimestampSerializer,
Expand Down Expand Up @@ -90,6 +91,8 @@
tagged_uint64,
float32,
float64,
bfloat16 as bfloat16_type,
bfloat16_array,
int8_array,
uint8_array,
int16_array,
Expand Down Expand Up @@ -119,6 +122,13 @@
)
from pyfory.policy import DeserializationPolicy # noqa: F401 # pylint: disable=unused-import

# BFloat16 support
from pyfory.bfloat16 import bfloat16 # noqa: F401
from pyfory.bfloat16_array import BFloat16Array # noqa: F401

# Keep compatibility with existing API naming.
BFloat16 = bfloat16

__version__ = "0.16.0.dev0"

__all__ = [
Expand Down Expand Up @@ -152,6 +162,10 @@
"tagged_uint64",
"float32",
"float64",
"BFloat16",
"BFloat16Array",
"bfloat16",
"bfloat16_array",
"int8_array",
"uint8_array",
"int16_array",
Expand Down Expand Up @@ -193,6 +207,7 @@
"TaggedUint64Serializer",
"Float32Serializer",
"Float64Serializer",
"BFloat16Serializer",
"StringSerializer",
"DateSerializer",
"TimestampSerializer",
Expand Down
24 changes: 24 additions & 0 deletions python/pyfory/bfloat16.pxd
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

from libc.stdint cimport uint16_t

cdef class bfloat16:
cdef uint16_t _bits


cdef bfloat16 bfloat16_from_bits(uint16_t bits)
130 changes: 130 additions & 0 deletions python/pyfory/bfloat16.pyx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

# distutils: language = c++
# cython: embedsignature = True
# cython: language_level = 3

from libc.stdint cimport uint16_t, uint32_t
from libc.string cimport memcpy

cdef inline uint16_t float32_to_bfloat16_bits(float value) nogil:
cdef uint32_t f32_bits
cdef uint16_t bf16_bits
cdef uint16_t truncated
memcpy(&f32_bits, &value, 4)
# Preserve NaN payloads and force the quiet-NaN bit so signaling NaNs do
# not collapse into infinity when the lower 16 payload bits are truncated.
if (f32_bits & 0x7FFFFFFF) > 0x7F800000:
return (<uint16_t>(f32_bits >> 16)) | 0x0040
bf16_bits = <uint16_t>(f32_bits >> 16)
truncated = <uint16_t>(f32_bits & 0xFFFF)
if truncated > 0x8000:
bf16_bits += 1
if (bf16_bits & 0x7F80) == 0x7F80:
bf16_bits = (bf16_bits & 0x8000) | 0x7F80
elif truncated == 0x8000 and (bf16_bits & 1):
bf16_bits += 1
if (bf16_bits & 0x7F80) == 0x7F80:
bf16_bits = (bf16_bits & 0x8000) | 0x7F80
return bf16_bits

cdef inline float bfloat16_bits_to_float32(uint16_t bits) nogil:
cdef uint32_t f32_bits = <uint32_t>bits << 16
cdef float result
memcpy(&result, &f32_bits, 4)
return result


cdef bfloat16 bfloat16_from_bits(uint16_t bits):
cdef bfloat16 value = bfloat16.__new__(bfloat16)
value._bits = bits
return value


cdef class bfloat16:
def __init__(self, value):
if isinstance(value, bfloat16):
self._bits = (<bfloat16>value)._bits
else:
self._bits = float32_to_bfloat16_bits(<float>float(value))

@staticmethod
def from_bits(uint16_t bits):
return bfloat16_from_bits(bits)

def to_bits(self):
return self._bits

def to_float32(self):
return bfloat16_bits_to_float32(self._bits)

def __float__(self):
return float(self.to_float32())

def __repr__(self):
return f"bfloat16({self.to_float32()})"

def __str__(self):
return str(self.to_float32())

def __eq__(self, other):
if isinstance(other, bfloat16):
if self.is_nan() or (<bfloat16>other).is_nan():
return False
if self.is_zero() and (<bfloat16>other).is_zero():
return True
return self._bits == (<bfloat16>other)._bits
return False

def __hash__(self):
if self.is_zero():
return hash(0)
return hash(self._bits)

def is_nan(self):
cdef uint16_t exp = (self._bits >> 7) & 0xFF
cdef uint16_t mant = self._bits & 0x7F
return exp == 0xFF and mant != 0

def is_inf(self):
cdef uint16_t exp = (self._bits >> 7) & 0xFF
cdef uint16_t mant = self._bits & 0x7F
return exp == 0xFF and mant == 0

def is_zero(self):
return (self._bits & 0x7FFF) == 0

def is_finite(self):
cdef uint16_t exp = (self._bits >> 7) & 0xFF
return exp != 0xFF

def is_normal(self):
cdef uint16_t exp = (self._bits >> 7) & 0xFF
return exp != 0 and exp != 0xFF

def is_subnormal(self):
cdef uint16_t exp = (self._bits >> 7) & 0xFF
cdef uint16_t mant = self._bits & 0x7F
return exp == 0 and mant != 0

def signbit(self):
return (self._bits & 0x8000) != 0


# Backward-compatible alias for existing user code.
BFloat16 = bfloat16
104 changes: 104 additions & 0 deletions python/pyfory/bfloat16_array.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

import array

from pyfory.bfloat16 import bfloat16
from pyfory.utils import is_little_endian


class BFloat16Array:
def __init__(self, values=None):
if values is None:
self._data = array.array("H")
elif isinstance(values, BFloat16Array):
self._data = array.array("H", values._data)
elif isinstance(values, array.array) and values.typecode == "H":
self._data = array.array("H", values)
else:
self._data = array.array(
"H",
(v.to_bits() if isinstance(v, bfloat16) else bfloat16(v).to_bits() for v in values),
)

def __len__(self):
return len(self._data)

def __getitem__(self, index):
return bfloat16.from_bits(self._data[index])

def __setitem__(self, index, value):
if isinstance(value, bfloat16):
self._data[index] = value.to_bits()
else:
self._data[index] = bfloat16(value).to_bits()

def __iter__(self):
for bits in self._data:
yield bfloat16.from_bits(bits)

def __repr__(self):
return f"BFloat16Array([{', '.join(str(bf16) for bf16 in self)}])"

def __eq__(self, other):
if not isinstance(other, BFloat16Array):
return False
return self._data == other._data

def append(self, value):
if isinstance(value, bfloat16):
self._data.append(value.to_bits())
else:
self._data.append(bfloat16(value).to_bits())

def extend(self, values):
if isinstance(values, BFloat16Array):
self._data.extend(values._data)
return
for value in values:
self.append(value)

@property
def itemsize(self):
return 2

def tobytes(self):
if is_little_endian:
return self._data.tobytes()
data = array.array("H", self._data)
data.byteswap()
return data.tobytes()

def to_bits_array(self):
return array.array("H", self._data)

@classmethod
def from_bits_array(cls, values):
arr = cls()
arr._data = array.array("H", values)
return arr

@classmethod
def frombytes(cls, data):
if len(data) % 2 != 0:
raise ValueError("bfloat16 byte payload length must be a multiple of 2")
arr = cls()
arr._data = array.array("H")
arr._data.frombytes(data)
if not is_little_endian:
arr._data.byteswap()
return arr
9 changes: 9 additions & 0 deletions python/pyfory/buffer.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ from cython.operator cimport dereference as deref
from libcpp.string cimport string as c_string
from libc.stdint cimport *
from libcpp cimport bool as c_bool
from pyfory.bfloat16 cimport bfloat16, bfloat16_from_bits
from pyfory.includes.libutil cimport(
CBuffer, COutputStream, allocate_buffer, get_bit as c_get_bit, set_bit as c_set_bit, clear_bit as c_clear_bit,
set_bit_to as c_set_bit_to, CError, CErrorCode, CResultVoidError, utf16_has_surrogate_pairs
Expand Down Expand Up @@ -380,6 +381,14 @@ cdef class Buffer:
cpdef inline write_float64(self, double value):
self.c_buffer.write_double(value)

cpdef inline write_bfloat16(self, uint16_t value):
self.c_buffer.write_uint16(value)

cpdef inline bfloat16 read_bfloat16(self):
cdef uint16_t value = self.c_buffer.read_uint16(self._error)
self._raise_if_error()
return bfloat16_from_bits(value)

cpdef put_buffer(self, uint32_t offset, v, int32_t src_index, int32_t length):
if length == 0: # access an emtpy buffer may raise out-of-bound exception.
return
Expand Down
3 changes: 3 additions & 0 deletions python/pyfory/codegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"write_nullable_pyfloat64",
"read_nullable_pyfloat64",
),
"bfloat16": ("write_bfloat16", "read_bfloat16", "write_nullable_bfloat16", "read_nullable_bfloat16"),
}


Expand Down Expand Up @@ -144,6 +145,8 @@ def compile_function(
context["read_nullable_pyfloat64"] = serialization.read_nullable_pyfloat64
context["write_nullable_pystr"] = serialization.write_nullable_pystr
context["read_nullable_pystr"] = serialization.read_nullable_pystr
context["write_nullable_bfloat16"] = serialization.write_nullable_bfloat16
context["read_nullable_bfloat16"] = serialization.read_nullable_bfloat16
stmts = [f"{ident(statement)}" for statement in stmts]
# Sanitize the function name to ensure it is valid Python syntax
sanitized_function_name = _sanitize_function_name(function_name)
Expand Down
Loading
Loading