diff --git a/src/bindings/python/src/openvino/helpers/packing.py b/src/bindings/python/src/openvino/helpers/packing.py index f85a84bfd0eb92..8ff664df5fc335 100644 --- a/src/bindings/python/src/openvino/helpers/packing.py +++ b/src/bindings/python/src/openvino/helpers/packing.py @@ -11,9 +11,8 @@ def pack_data(array: np.ndarray, type: Type) -> np.ndarray: """Represent array values as u1, u2, u3, u4, u6 or i4 openvino element type and pack them into uint8 numpy array. - For u1, u4, i4: Standard bit packing where 8 % bitwidth == 0 - For u3: Transposed packing - 8 values in 3 bytes - For u6: Transposed packing - 4 values in 3 bytes + For u1, u2, u4, i4: Standard bit packing where 8 % bitwidth == 0 + For u3, u6: Linear LSB-first bit-stream packing, values may straddle a byte boundary If the number of elements in array is odd we pad them with zero value to be able to fit the bit sequence into the uint8 array. @@ -27,11 +26,11 @@ def pack_data(array: np.ndarray, type: Type) -> np.ndarray: :param type: Type to interpret the array values. Type must be u1, u2, u3, u4, u6, i4, nf4 or f4e2m1. :type type: openvino.Type """ - # Handle u3 and u6 with special transposed packing + # Handle u3 and u6 with linear LSB-first bit-stream packing if type == Type.u3: - return _pack_u3(array) + return _pack_linear(array, 3) elif type == Type.u6: - return _pack_u6(array) + return _pack_linear(array, 6) assert type in [Type.u1, Type.u2, Type.u4, Type.i4, Type.nf4, Type.f4e2m1], "Packing algorithm for the" "data types stored in 1, 2 or 4 bits" @@ -61,9 +60,8 @@ def pack_data(array: np.ndarray, type: Type) -> np.ndarray: def unpack_data(array: np.ndarray, type: Type, shape: Union[list, Shape]) -> np.ndarray: """Extract openvino element type values from array into new uint8/int8 array given shape. - For u1, u4, i4: Standard bit unpacking where 8 % bitwidth == 0 - For u3: Transposed unpacking - 8 values from 3 bytes - For u6: Transposed unpacking - 4 values from 3 bytes + For u1, u2, u4, i4: Standard bit unpacking where 8 % bitwidth == 0 + For u3, u6: Linear LSB-first bit-stream unpacking, values may straddle a byte boundary Example: uint8 value [120] can be represented as two u4 values and be unpacked into [7, 8] because [120] bit representation is [01111000] will be viewed as [0111, 1000], @@ -76,11 +74,11 @@ def unpack_data(array: np.ndarray, type: Type, shape: Union[list, Shape]) -> np. :param shape: the new shape for the unpacked array. :type shape: Union[list, openvino.Shape] """ - # Handle u3 and u6 with special transposed unpacking + # Handle u3 and u6 with linear LSB-first bit-stream unpacking if type == Type.u3: - return _unpack_u3(array, shape) + return _unpack_linear(array, 3, shape) elif type == Type.u6: - return _unpack_u6(array, shape) + return _unpack_linear(array, 6, shape) assert type in [Type.u1, Type.u2, Type.u4, Type.i4, Type.nf4, Type.f4e2m1], "Unpacking algorithm for the" "data types stored in 1, 2 or 4 bits" unpacked = np.unpackbits(array.view(np.uint8)) @@ -107,148 +105,23 @@ def unpack_data(array: np.ndarray, type: Type, shape: Union[list, Shape]) -> np. return np.resize(packed, shape) -def _pack_u3(array: np.ndarray) -> np.ndarray: - """Pack u3 values using transposed packing scheme. - - 8 values (each 3 bits) are packed into 3 bytes: - - Byte 0: bits [1:0] of values 0-3 (4 values * 2 bits = 8 bits) - - Byte 1: bits [1:0] of values 4-7 (4 values * 2 bits = 8 bits) - - Byte 2: bits [2] of all 8 values (8 values * 1 bit = 8 bits) +def _pack_linear(array: np.ndarray, bits: int) -> np.ndarray: + """Pack values into a linear, LSB-first bit-stream (used for u3/u6): value i occupies bits + [i * bits, i * bits + bits), possibly straddling a byte boundary. """ array = array.astype(np.uint8, casting="unsafe").flatten() - # Pad to multiple of 8 - pad = (-len(array)) % 8 - if pad: - array = np.concatenate([array, np.zeros(pad, dtype=np.uint8)]) - - groups = array.reshape(-1, 8) - result = [] - - for group in groups: - # Extract lower 2 bits and MSB for each value - lower_bits = group & 0x03 # bits [1:0] - msb = (group >> 2) & 0x01 # bit [2] - - # Pack into 3 bytes - byte0 = (lower_bits[0] << 6) | (lower_bits[1] << 4) | (lower_bits[2] << 2) | lower_bits[3] - byte1 = (lower_bits[4] << 6) | (lower_bits[5] << 4) | (lower_bits[6] << 2) | lower_bits[7] - byte2 = (msb[0] << 7) | (msb[1] << 6) | (msb[2] << 5) | (msb[3] << 4) | \ - (msb[4] << 3) | (msb[5] << 2) | (msb[6] << 1) | msb[7] + bit_order_little = (array[:, None] & (1 << np.arange(bits)) > 0).astype(np.uint8) + return np.packbits(bit_order_little.flatten(), bitorder="little") - result.extend([byte0, byte1, byte2]) - return np.array(result, dtype=np.uint8) - - -def _unpack_u3(array: np.ndarray, shape: Union[list, Shape]) -> np.ndarray: - """Unpack u3 values using transposed unpacking scheme. - - 3 bytes are unpacked into 8 values (each 3 bits). - """ - array = array.view(np.uint8) - shape = list(shape) - - # Process 3 bytes at a time - result = [] - for i in range(0, len(array), 3): - if i + 2 >= len(array): - break - byte0, byte1, byte2 = array[i:i+3] - - # Unpack lower 2 bits from first two bytes - val0 = (byte0 >> 6) & 0x03 - val1 = (byte0 >> 4) & 0x03 - val2 = (byte0 >> 2) & 0x03 - val3 = byte0 & 0x03 - val4 = (byte1 >> 6) & 0x03 - val5 = (byte1 >> 4) & 0x03 - val6 = (byte1 >> 2) & 0x03 - val7 = byte1 & 0x03 - - # Unpack MSBs from third byte - msb0 = (byte2 >> 7) & 0x01 - msb1 = (byte2 >> 6) & 0x01 - msb2 = (byte2 >> 5) & 0x01 - msb3 = (byte2 >> 4) & 0x01 - msb4 = (byte2 >> 3) & 0x01 - msb5 = (byte2 >> 2) & 0x01 - msb6 = (byte2 >> 1) & 0x01 - msb7 = byte2 & 0x01 - - # Combine to form 3-bit values - result.extend([ - val0 | (msb0 << 2), val1 | (msb1 << 2), val2 | (msb2 << 2), val3 | (msb3 << 2), - val4 | (msb4 << 2), val5 | (msb5 << 2), val6 | (msb6 << 2), val7 | (msb7 << 2) - ]) - - result = np.array(result, dtype=np.uint8) - return np.resize(result, shape) - - -def _pack_u6(array: np.ndarray) -> np.ndarray: - """Pack u6 values using transposed packing scheme. - - 4 values (each 6 bits) are packed into 3 bytes: - - Byte 0: bits [3:0] of values 0-1 (2 values * 4 bits = 8 bits) - - Byte 1: bits [3:0] of values 2-3 (2 values * 4 bits = 8 bits) - - Byte 2: bits [5:4] of all 4 values (4 values * 2 bits = 8 bits) - """ - array = array.astype(np.uint8, casting="unsafe").flatten() - # Pad to multiple of 4 - pad = (-len(array)) % 4 - if pad: - array = np.concatenate([array, np.zeros(pad, dtype=np.uint8)]) - - groups = array.reshape(-1, 4) - result = [] - - for group in groups: - lower_bits = group & 0x0F # bits [3:0] - upper_bits = (group >> 4) & 0x03 # bits [5:4] - - # Pack into 3 bytes - byte0 = (lower_bits[0] << 4) | lower_bits[1] - byte1 = (lower_bits[2] << 4) | lower_bits[3] - byte2 = (upper_bits[0] << 6) | (upper_bits[1] << 4) | (upper_bits[2] << 2) | upper_bits[3] - - result.extend([byte0, byte1, byte2]) - - return np.array(result, dtype=np.uint8) - - -def _unpack_u6(array: np.ndarray, shape: Union[list, Shape]) -> np.ndarray: - """Unpack u6 values using transposed unpacking scheme. - - 3 bytes are unpacked into 4 values (each 6 bits). - """ +def _unpack_linear(array: np.ndarray, bits: int, shape: Union[list, Shape]) -> np.ndarray: + """Unpack values from a linear, LSB-first bit-stream (used for u3/u6), inverse of _pack_linear.""" array = array.view(np.uint8) shape = list(shape) num_values = int(np.prod(shape)) - - # Process 3 bytes at a time - result = [] - for i in range(0, len(array), 3): - if i + 2 >= len(array): - break - byte0, byte1, byte2 = array[i:i+3] - - # Unpack lower 4 bits from first two bytes - val0 = (byte0 >> 4) & 0x0F - val1 = byte0 & 0x0F - val2 = (byte1 >> 4) & 0x0F - val3 = byte1 & 0x0F - - # Unpack upper 2 bits from third byte - upper0 = (byte2 >> 6) & 0x03 - upper1 = (byte2 >> 4) & 0x03 - upper2 = (byte2 >> 2) & 0x03 - upper3 = byte2 & 0x03 - - # Combine to form 6-bit values - result.extend([ - val0 | (upper0 << 4), val1 | (upper1 << 4), - val2 | (upper2 << 4), val3 | (upper3 << 4) - ]) - - result = np.array(result, dtype=np.uint8) + + bits_unpacked = np.unpackbits(array, bitorder="little")[: num_values * bits] + values = bits_unpacked.reshape(-1, bits) + weights = (1 << np.arange(bits)).astype(np.uint8) + result = (values * weights).sum(axis=1).astype(np.uint8) return np.resize(result, shape) diff --git a/src/bindings/python/src/openvino/helpers/packing.pyi b/src/bindings/python/src/openvino/helpers/packing.pyi index d1070cae5b9208..5907e82a2e6036 100644 --- a/src/bindings/python/src/openvino/helpers/packing.pyi +++ b/src/bindings/python/src/openvino/helpers/packing.pyi @@ -6,47 +6,22 @@ import numpy import numpy as np import openvino._pyopenvino __all__: list[str] = ['Shape', 'Type', 'np', 'pack_data', 'unpack_data'] -def _pack_u3(array: numpy.ndarray) -> numpy.ndarray: +def _pack_linear(array: numpy.ndarray, bits: int) -> numpy.ndarray: """ - Pack u3 values using transposed packing scheme. - - 8 values (each 3 bits) are packed into 3 bytes: - - Byte 0: bits [1:0] of values 0-3 (4 values * 2 bits = 8 bits) - - Byte 1: bits [1:0] of values 4-7 (4 values * 2 bits = 8 bits) - - Byte 2: bits [2] of all 8 values (8 values * 1 bit = 8 bits) - - """ -def _pack_u6(array: numpy.ndarray) -> numpy.ndarray: - """ - Pack u6 values using transposed packing scheme. - - 4 values (each 6 bits) are packed into 3 bytes: - - Byte 0: bits [3:0] of values 0-1 (2 values * 4 bits = 8 bits) - - Byte 1: bits [3:0] of values 2-3 (2 values * 4 bits = 8 bits) - - Byte 2: bits [5:4] of all 4 values (4 values * 2 bits = 8 bits) + Pack values into a linear, LSB-first bit-stream (used for u3/u6): value i occupies bits + [i * bits, i * bits + bits), possibly straddling a byte boundary. """ -def _unpack_u3(array: numpy.ndarray, shape: typing.Union[list, openvino._pyopenvino.Shape]) -> numpy.ndarray: +def _unpack_linear(array: numpy.ndarray, bits: int, shape: typing.Union[list, openvino._pyopenvino.Shape]) -> numpy.ndarray: """ - Unpack u3 values using transposed unpacking scheme. - - 3 bytes are unpacked into 8 values (each 3 bits). - - """ -def _unpack_u6(array: numpy.ndarray, shape: typing.Union[list, openvino._pyopenvino.Shape]) -> numpy.ndarray: - """ - Unpack u6 values using transposed unpacking scheme. - - 3 bytes are unpacked into 4 values (each 6 bits). - + Unpack values from a linear, LSB-first bit-stream (used for u3/u6), inverse of _pack_linear. """ def pack_data(array: numpy.ndarray, type: openvino._pyopenvino.Type) -> numpy.ndarray: """ Represent array values as u1, u2, u3, u4, u6 or i4 openvino element type and pack them into uint8 numpy array. - For u1, u4, i4: Standard bit packing where 8 % bitwidth == 0 - For u3: Transposed packing - 8 values in 3 bytes - For u6: Transposed packing - 4 values in 3 bytes + For u1, u2, u4, i4: Standard bit packing where 8 % bitwidth == 0 + For u3, u6: Linear LSB-first bit-stream packing, values may straddle a byte boundary If the number of elements in array is odd we pad them with zero value to be able to fit the bit sequence into the uint8 array. @@ -65,9 +40,8 @@ def unpack_data(array: numpy.ndarray, type: openvino._pyopenvino.Type, shape: ty """ Extract openvino element type values from array into new uint8/int8 array given shape. - For u1, u4, i4: Standard bit unpacking where 8 % bitwidth == 0 - For u3: Transposed unpacking - 8 values from 3 bytes - For u6: Transposed unpacking - 4 values from 3 bytes + For u1, u2, u4, i4: Standard bit unpacking where 8 % bitwidth == 0 + For u3, u6: Linear LSB-first bit-stream unpacking, values may straddle a byte boundary Example: uint8 value [120] can be represented as two u4 values and be unpacked into [7, 8] because [120] bit representation is [01111000] will be viewed as [0111, 1000], diff --git a/src/bindings/python/tests/test_runtime/test_tensor.py b/src/bindings/python/tests/test_runtime/test_tensor.py index 6fec354357c59b..c0022be4bb53f6 100644 --- a/src/bindings/python/tests/test_runtime/test_tensor.py +++ b/src/bindings/python/tests/test_runtime/test_tensor.py @@ -683,18 +683,14 @@ def test_tensor_from_pillow(numpy_dtype, shape): def test_pack_unpack_u3_numerical(): """Test u3 packing/unpacking with known numerical values.""" - # 8 values packed into 3 bytes + # 8 values packed into 3 bytes, linear LSB-first bit-stream (value i at bits [3i, 3i+3)) # Decimal values: [0, 1, 2, 3, 4, 5, 6, 7] # In binary: [000, 001, 010, 011, 100, 101, 110, 111] data = np.array([0, 1, 2, 3, 4, 5, 6, 7], dtype=np.uint8) packed = pack_data(data, ov.Type.u3) - # Expected packing: - # Byte 0: bits[1:0] of values 0-3 = [00, 01, 10, 11] = 0x1B - # Byte 1: bits[1:0] of values 4-7 = [00, 01, 10, 11] = 0x1B - # Byte 2: bits[2] of all 8 values = [0,0,0,0,1,1,1,1] = 0x0F - expected = np.array([0x1B, 0x1B, 0x0F], dtype=np.uint8) + expected = np.array([0x88, 0xC6, 0xFA], dtype=np.uint8) assert len(packed) == 3 assert np.array_equal(packed, expected), f"Expected {expected.tolist()}, got {packed.tolist()}" @@ -721,17 +717,13 @@ def test_pack_unpack_u3_edge_cases(): def test_pack_unpack_u6_numerical(): - # 4 values packed into 3 bytes + # 4 values packed into 3 bytes, linear LSB-first bit-stream (value i at bits [6i, 6i+6)) # Decimal values: [0, 15, 48, 63] # In binary: [000000, 001111, 110000, 111111] data = np.array([0, 15, 48, 63], dtype=np.uint8) packed = pack_data(data, ov.Type.u6) - # Expected packing: - # Byte 0: bits[3:0] of values 0-1 = [0000, 1111] = 0x0F - # Byte 1: bits[3:0] of values 2-3 = [0000, 1111] = 0x0F - # Byte 2: bits[5:4] of all 4 values = [00, 00, 11, 11] = 0x0F - expected = np.array([0x0F, 0x0F, 0x0F], dtype=np.uint8) + expected = np.array([0xC0, 0x03, 0xFF], dtype=np.uint8) assert len(packed) == 3 assert np.array_equal(packed, expected), f"Expected {expected.tolist()}, got {packed.tolist()}" diff --git a/src/core/dev_api/openvino/core/type/element_iterator.hpp b/src/core/dev_api/openvino/core/type/element_iterator.hpp index d6c9593e61fe7f..7680c4226ffeb6 100644 --- a/src/core/dev_api/openvino/core/type/element_iterator.hpp +++ b/src/core/dev_api/openvino/core/type/element_iterator.hpp @@ -48,7 +48,8 @@ constexpr bool is_nibble_type(Type_t et) { /** * @brief Checks if element type is split bit type. * - * The value is stored in byte(s) like [b0, b1, x, .., x, b2, b3]. + * The value is packed LSB-first as a linear, cross-byte bit-stream: value `i` occupies bits + * `[i * bitwidth, i * bitwidth + bitwidth)` of the stream and may straddle a byte boundary. * * @param et Element type to check * @return True if element type is split bit type otherwise false. @@ -294,7 +295,8 @@ class BitProxy> { /** * @brief The BitProxy specialization for u3, u6 precisions. * - * @note The input pointer must point on buffer which has got 3 * n bytes. + * Values are packed LSB-first as a linear bit-stream and may straddle a byte boundary, so a value + * spans at most 2 bytes (bit width <= 6, intra-byte offset <= 7). * * @tparam T Fundamental type of sub-byte value which must be same as fundamental type of element::Type_t. * @tparam ET OpenVINO element type. @@ -305,24 +307,14 @@ class BitProxy> { template friend class Iterator; //!< Iterator class is friend to access private members to manipulate pointer. - static constexpr size_t m_bits = bit_width(); //!< Number of bit for single value. - static constexpr size_t m_num_values = (3 * 8) / m_bits; //!< Number values in byte. - static constexpr size_t m_shift_init = m_num_values - 1; //!< Initial value for bit shift. - - struct ByteValue { - uint8_t b0; - uint8_t b1; - uint8_t b2; - }; + using Bits = std::conditional_t, const uint8_t, uint8_t>; - union { - T* m_ptr; //!< Pointer to T buffer. - ByteValue* m_bytes; //!< Pointer to buffer as 3 bytes representation. - }; + static constexpr size_t m_bits = bit_width(); //!< Number of bit for single value. - size_t m_bit_shift; //!< Current bit shift to get value. + Bits* m_ptr; //!< Pointer to byte containing the first bit of the value. + size_t m_bit_shift; //!< Offset (0..7) of the value's first bit within *m_ptr. - constexpr BitProxy(T* ptr) noexcept : m_ptr{ptr}, m_bit_shift{m_shift_init} {} + constexpr BitProxy(T* ptr) noexcept : m_ptr{reinterpret_cast(ptr)}, m_bit_shift{0} {} public: using value_type = std::decay_t; //!< Fundamental type of sub-byte. @@ -357,17 +349,14 @@ class BitProxy> { * @return Value of BitProxy. */ operator value_type() const { - constexpr uint16_t lower_mask_bits = 16 / m_num_values; - constexpr uint16_t upper_mask_bits = 8 / m_num_values; - constexpr uint16_t mask_lower = util::make_n_bit_mask(lower_mask_bits); - constexpr uint16_t mask_upper = util::make_n_bit_mask(upper_mask_bits) << lower_mask_bits; - - // get lower part of value - uint16_t v = ((m_bytes->b0 << 8U) | m_bytes->b1) >> (lower_mask_bits * m_bit_shift); - v &= mask_lower; - // get upper part of value - v |= ((m_bytes->b2 << lower_mask_bits) >> (upper_mask_bits * m_bit_shift)) & mask_upper; - return static_cast(v); + constexpr auto mask = static_cast(util::make_n_bit_mask(m_bits)); + // Only touch the 2nd byte when the value actually straddles into it (avoids reading past + // the end of the allocated buffer for the last value in a tensor). + uint16_t w = static_cast(m_ptr[0]); + if (m_bit_shift + m_bits > 8) { + w |= static_cast(m_ptr[1]) << 8; + } + return static_cast((w >> m_bit_shift) & mask); } /** @@ -375,20 +364,16 @@ class BitProxy> { * @param v Value to be set. */ BitProxy& operator=(const value_type v) { - constexpr uint16_t lower_mask_bits = 16 / m_num_values; - constexpr uint16_t upper_mask_bits = 8 / m_num_values; - constexpr uint16_t mask_lower = util::make_n_bit_mask(lower_mask_bits); - constexpr uint16_t mask_upper = util::make_n_bit_mask(upper_mask_bits) << lower_mask_bits; - - uint16_t tmp = (m_bytes->b0 << 8U) | m_bytes->b1; - tmp &= ~(mask_lower << (lower_mask_bits * m_bit_shift)); - tmp |= (v & mask_lower) << (lower_mask_bits * m_bit_shift); - m_bytes->b0 = tmp >> 8U; - m_bytes->b1 = tmp & 0x00ff; - - tmp = m_bytes->b2 & ~((mask_upper >> lower_mask_bits) << (upper_mask_bits * m_bit_shift)); - tmp |= (((v & mask_upper) >> lower_mask_bits) << (upper_mask_bits * m_bit_shift)); - m_bytes->b2 = tmp & 0x00ff; + constexpr auto mask = static_cast(util::make_n_bit_mask(m_bits)); + const auto shifted_mask = static_cast(mask << m_bit_shift); + const auto shifted_value = static_cast((static_cast(v) & mask) << m_bit_shift); + + m_ptr[0] = static_cast((m_ptr[0] & ~static_cast(shifted_mask & 0xffU)) | + static_cast(shifted_value & 0xffU)); + if (m_bit_shift + m_bits > 8) { + m_ptr[1] = static_cast((m_ptr[1] & ~static_cast(shifted_mask >> 8U)) | + static_cast(shifted_value >> 8U)); + } return *this; } }; @@ -442,9 +427,11 @@ class Iterator { m_et_ptr.m_bit_shift ^= m_et_ptr.m_bits; m_et_ptr.m_ptr += static_cast(m_et_ptr.m_bit_shift == m_et_ptr.m_shift_init); } else if constexpr (is_split_bit_type(ET)) { - --m_et_ptr.m_bit_shift; - m_et_ptr.m_bit_shift = m_et_ptr.m_bit_shift % m_et_ptr.m_num_values; - m_et_ptr.m_ptr += (m_et_ptr.m_bit_shift == m_et_ptr.m_shift_init) ? 3 : 0; + m_et_ptr.m_bit_shift += m_et_ptr.m_bits; + if (m_et_ptr.m_bit_shift >= 8) { + m_et_ptr.m_bit_shift -= 8; + ++m_et_ptr.m_ptr; + } } else { if constexpr (is_lsb_packed(ET)) { m_et_ptr.m_bit_shift += m_et_ptr.m_bits; @@ -470,9 +457,17 @@ class Iterator { ++*this; } } else if constexpr (is_split_bit_type(ET)) { - const auto advance = n + m_et_ptr.m_shift_init - m_et_ptr.m_bit_shift; - m_et_ptr.m_bit_shift = m_et_ptr.m_shift_init - (advance % m_et_ptr.m_num_values); - m_et_ptr.m_ptr += 3 * (advance / m_et_ptr.m_num_values); + // floor-divide the new absolute bit position by 8 to get byte advance + new offset + const auto total_bits = static_cast(m_et_ptr.m_bit_shift) + + n * static_cast(m_et_ptr.m_bits); + auto div = total_bits / 8; + auto rem = total_bits % 8; + if (rem < 0) { + rem += 8; + --div; + } + m_et_ptr.m_bit_shift = static_cast(rem); + m_et_ptr.m_ptr += div; } else if constexpr (is_lsb_packed(ET)) { const auto advance = n + m_et_ptr.m_bit_shift / m_et_ptr.m_bits; m_et_ptr.m_bit_shift = (advance % m_et_ptr.m_num_values) * m_et_ptr.m_bits; @@ -496,9 +491,11 @@ class Iterator { m_et_ptr.m_bit_shift ^= m_et_ptr.m_bits; m_et_ptr.m_ptr -= static_cast(m_et_ptr.m_bit_shift == 4); } else if constexpr (is_split_bit_type(ET)) { - ++m_et_ptr.m_bit_shift; - m_et_ptr.m_bit_shift = m_et_ptr.m_bit_shift % m_et_ptr.m_num_values; - m_et_ptr.m_ptr -= m_et_ptr.m_bit_shift == 0 ? 3 : 0; + if (m_et_ptr.m_bit_shift < m_et_ptr.m_bits) { + m_et_ptr.m_bit_shift += 8; + --m_et_ptr.m_ptr; + } + m_et_ptr.m_bit_shift -= m_et_ptr.m_bits; } else { if constexpr (is_lsb_packed(ET)) { m_et_ptr.m_bit_shift -= m_et_ptr.m_bits; @@ -524,9 +521,17 @@ class Iterator { --*this; } } else if constexpr (is_split_bit_type(ET)) { - const auto advance = m_et_ptr.m_bit_shift + n; - m_et_ptr.m_bit_shift = advance % m_et_ptr.m_num_values; - m_et_ptr.m_ptr -= 3 * (advance / m_et_ptr.m_num_values); + // same as operator+=(-n): floor-divide the new absolute bit position by 8 + const auto total_bits = static_cast(m_et_ptr.m_bit_shift) - + n * static_cast(m_et_ptr.m_bits); + auto div = total_bits / 8; + auto rem = total_bits % 8; + if (rem < 0) { + rem += 8; + --div; + } + m_et_ptr.m_bit_shift = static_cast(rem); + m_et_ptr.m_ptr += div; } else if constexpr (is_lsb_packed(ET)) { const auto advance = n + (m_et_ptr.m_shift_last - m_et_ptr.m_bit_shift) / m_et_ptr.m_bits; m_et_ptr.m_bit_shift = m_et_ptr.m_shift_last - (advance % m_et_ptr.m_num_values) * m_et_ptr.m_bits; diff --git a/src/core/reference/include/openvino/reference/transpose.hpp b/src/core/reference/include/openvino/reference/transpose.hpp index 855593f0746cb9..2d5b7f0f0e2256 100644 --- a/src/core/reference/include/openvino/reference/transpose.hpp +++ b/src/core/reference/include/openvino/reference/transpose.hpp @@ -11,6 +11,7 @@ #include #include "openvino/core/shape.hpp" +#include "openvino/core/type/element_type.hpp" namespace ov { namespace reference { @@ -70,5 +71,22 @@ void transpose_2bit(const uint8_t* data, const std::vector& axes_order, const Shape& out_shape); +/** + * @brief Reference implementation of Transpose operator for u3/u6 element types. + * + * @param data Pointer to input data (packed sub-byte values). + * @param out Pointer to output data (packed sub-byte values). + * @param data_shape Input data shape. + * @param axes_order Transpose order. + * @param out_shape Output data shape. + * @param arg_type Element type of data, must be u3 or u6. + */ +void transpose_subbyte(const uint8_t* data, + uint8_t* out, + const Shape& data_shape, + const std::vector& axes_order, + const Shape& out_shape, + const element::Type& arg_type); + } // namespace reference } // namespace ov diff --git a/src/core/reference/src/op/transpose.cpp b/src/core/reference/src/op/transpose.cpp index ccec1a52b21fe1..91c9ce753b50b9 100644 --- a/src/core/reference/src/op/transpose.cpp +++ b/src/core/reference/src/op/transpose.cpp @@ -129,14 +129,16 @@ void transpose_4bit(const uint8_t* data, } } -void transpose_2bit(const uint8_t* data, - uint8_t* out, - const Shape& data_shape, - const std::vector& axes_order, - const Shape& out_shape) { +namespace { +template +void transpose_by_iterator(const uint8_t* data, + uint8_t* out, + const Shape& data_shape, + const std::vector& axes_order, + const Shape& out_shape) { const size_t ndim = data_shape.size(); - auto in_it = ov::element::iterator(reinterpret_cast(data)); - auto out_it = ov::element::iterator(reinterpret_cast(out)); + auto in_it = ov::element::iterator(reinterpret_cast(data)); + auto out_it = ov::element::iterator(reinterpret_cast(out)); ov::Coordinate src_coord(ndim); const ov::CoordinateTransformBasic dst_transform{out_shape}; @@ -149,6 +151,30 @@ void transpose_2bit(const uint8_t* data, *(out_it + dst_idx) = *(in_it + src_idx); } } +} // namespace + +void transpose_2bit(const uint8_t* data, + uint8_t* out, + const Shape& data_shape, + const std::vector& axes_order, + const Shape& out_shape) { + transpose_by_iterator(data, out, data_shape, axes_order, out_shape); +} + +void transpose_subbyte(const uint8_t* data, + uint8_t* out, + const Shape& data_shape, + const std::vector& axes_order, + const Shape& out_shape, + const element::Type& arg_type) { + if (arg_type == ov::element::u3) { + transpose_by_iterator(data, out, data_shape, axes_order, out_shape); + } else if (arg_type == ov::element::u6) { + transpose_by_iterator(data, out, data_shape, axes_order, out_shape); + } else { + OPENVINO_THROW("transpose_subbyte supports only u3 and u6 element types, got: ", arg_type); + } +} } // namespace reference } // namespace ov diff --git a/src/core/src/memory_util.cpp b/src/core/src/memory_util.cpp index 9d11c9d9210af1..65fc4d54d22e24 100644 --- a/src/core/src/memory_util.cpp +++ b/src/core/src/memory_util.cpp @@ -10,21 +10,15 @@ namespace ov::util { namespace { -constexpr size_t split_unit_bit_size = 24; -constexpr size_t split_unit_byte_size = split_unit_bit_size / 8; +// u3/u6 use a linear, LSB-first bit-stream layout (values may straddle byte boundaries). size_t get_split_bit_memory_size(const element::Type& type, const size_t elements_count) { - const size_t elements_per_storage_unit = split_unit_bit_size / type.bitwidth(); - auto units_count = elements_count / elements_per_storage_unit; - units_count += static_cast(units_count * elements_per_storage_unit != elements_count); - return units_count * split_unit_byte_size; + size_t used_bits; + OPENVINO_ASSERT(!mul_overflow(elements_count, type.bitwidth(), used_bits)); + return (used_bits + 7) / 8; } size_t get_split_elements_count(const element::Type& type, const size_t memory_size) { - const size_t elements_per_storage_unit = split_unit_bit_size / type.bitwidth(); - const size_t storage_unit_count = memory_size / split_unit_byte_size; - size_t elements_count; - OPENVINO_ASSERT(!mul_overflow(storage_unit_count, elements_per_storage_unit, elements_count)); - return elements_count; + return (memory_size * 8) / type.bitwidth(); } size_t get_bit_memory_size(const element::Type& type, const size_t shape_size) { diff --git a/src/core/src/op/constant.cpp b/src/core/src/op/constant.cpp index 21a50561be547d..e489213bd04110 100644 --- a/src/core/src/op/constant.cpp +++ b/src/core/src/op/constant.cpp @@ -301,15 +301,14 @@ void Constant::set_unused_bits(void* buffer) const { constexpr size_t storage_unit_byte_size = 1; reinterpret_cast(buffer)[byte_size - storage_unit_byte_size] &= 0x0FU; } else if (element::is_split_bit_type(m_element_type)) { - constexpr size_t storage_unit_byte_size = 3; - const auto num_values = (24U / m_element_type.bitwidth()); - const auto not_aligned_elements = num_elements % num_values; - const uint16_t not_used_upper_mask = ~(0xffff >> (not_aligned_elements * (16U / num_values))); - - auto ptr = reinterpret_cast(buffer) + (byte_size - storage_unit_byte_size); - ptr[0] &= not_used_upper_mask >> 8U; - ptr[1] &= not_used_upper_mask & 0x00ff; - ptr[2] &= ~(0xff >> (not_aligned_elements * (8U / num_values))); + // Linear LSB-first bit-stream: mask off the unused tail bits of the last partial byte. + // Any fully-unused trailing bytes are already excluded from the allocation. + const auto used_bits = num_elements * m_element_type.bitwidth(); + const auto tail_bits = used_bits % 8; + if (tail_bits != 0) { + const uint8_t used_bits_mask = static_cast(0xffU >> (8 - tail_bits)); + reinterpret_cast(buffer)[byte_size - 1] &= used_bits_mask; + } } } } diff --git a/src/core/src/op/transpose.cpp b/src/core/src/op/transpose.cpp index e7b79c435005d2..10a44fe939650e 100644 --- a/src/core/src/op/transpose.cpp +++ b/src/core/src/op/transpose.cpp @@ -67,6 +67,13 @@ bool Transpose::evaluate(TensorVector& outputs, const TensorVector& inputs) cons arg.get_shape(), axes_order, out_shape); + } else if (arg_type == ov::element::u3 || arg_type == ov::element::u6) { + reference::transpose_subbyte(static_cast(arg.data()), + static_cast(out.data()), + arg.get_shape(), + axes_order, + out_shape, + arg_type); } else if (arg_type == ov::element::string) { reference::transpose(static_cast(arg.data()), static_cast(out.data()), diff --git a/src/core/tests/constant.cpp b/src/core/tests/constant.cpp index ef29539b3c6a26..d1ff0c8a558acb 100644 --- a/src/core/tests/constant.cpp +++ b/src/core/tests/constant.cpp @@ -970,9 +970,9 @@ TEST(constant, uint3_string) { EXPECT_THAT(v, ElementsAre(3, 0, 1, 2, 4, 7, 5, 6)); const auto p = c.get_data_ptr(); - EXPECT_EQ(p[0], 0b11000110); - EXPECT_EQ(p[1], 0b00110110); - EXPECT_EQ(p[2], 0b00001111); + EXPECT_EQ(p[0], 0b01000011); + EXPECT_EQ(p[1], 0b11000100); + EXPECT_EQ(p[2], 0b11010111); EXPECT_EQ(c.convert_value_to_string(6), "5"); EXPECT_THAT(c.get_value_strings(), ElementsAre("3", "0", "1", "2", "4", "7", "5", "6")); @@ -989,9 +989,8 @@ TEST(constant, uint3_string_broadcast) { EXPECT_THAT(v, Each(5)); const auto p = c.get_data_ptr(); - EXPECT_EQ(p[0], 0b01010101); - EXPECT_EQ(p[1], 0b01000000); - EXPECT_EQ(p[2], 0b11111000); + EXPECT_EQ(p[0], 0b01101101); + EXPECT_EQ(p[1], 0b01011011); } TEST(constant, uint3_vector_less_than_one_storage_unit) { @@ -1005,9 +1004,8 @@ TEST(constant, uint3_vector_less_than_one_storage_unit) { EXPECT_THAT(v, ElementsAre(5, 3, 1)); const auto p = c.get_vector(); - EXPECT_EQ(p[0], 0b01110100); + EXPECT_EQ(p[0], 0b01011101); EXPECT_EQ(p[1], 0); - EXPECT_EQ(p[2], 0b10000000); } TEST(constant, uint3_vector_greater_than_one_storage_unit) { @@ -1021,13 +1019,10 @@ TEST(constant, uint3_vector_greater_than_one_storage_unit) { EXPECT_THAT(v, ElementsAre(2, 3, 1, 0, 4, 5, 6, 7, 5, 2)); const auto p = c.get_vector(); - EXPECT_EQ(p[0], 0b10110100); - EXPECT_EQ(p[1], 0b00011011); - EXPECT_EQ(p[2], 0b00001111); - - EXPECT_EQ(p[3], 0b01100000); - EXPECT_EQ(p[4], 0); - EXPECT_EQ(p[5], 0b10000000); + EXPECT_EQ(p[0], 0b01011010); + EXPECT_EQ(p[1], 0b11000000); + EXPECT_EQ(p[2], 0b11111010); + EXPECT_EQ(p[3], 0b00010101); } TEST(constant, uint3_vector_broadcast) { @@ -1039,9 +1034,9 @@ TEST(constant, uint3_vector_broadcast) { EXPECT_THAT(v, Each(2)); const auto p = c.get_data_ptr(); - EXPECT_EQ(p[0], 0b10101010); - EXPECT_EQ(p[1], 0b10101010); - EXPECT_EQ(p[2], 0b00000000); + EXPECT_EQ(p[0], 0b10010010); + EXPECT_EQ(p[1], 0b00100100); + EXPECT_EQ(p[2], 0b01001001); } TEST(constant, uint3_write_then_cast_custom_type) { @@ -1173,9 +1168,9 @@ TEST(constant, uint6_string) { EXPECT_THAT(v, ElementsAre(4, 9, 15, 16)); const auto p = c.get_data_ptr(); - EXPECT_EQ(p[0], 0x49); - EXPECT_EQ(p[1], 0xf0); - EXPECT_EQ(p[2], 0b00000001); + EXPECT_EQ(p[0], 0x44); + EXPECT_EQ(p[1], 0xf2); + EXPECT_EQ(p[2], 0x40); EXPECT_EQ(c.convert_value_to_string(2), "15"); EXPECT_THAT(c.get_value_strings(), ElementsAre("4", "9", "15", "16")); @@ -1192,9 +1187,9 @@ TEST(constant, uint6_string_broadcast) { EXPECT_THAT(v, Each(5)); const auto p = c.get_data_ptr(); - EXPECT_EQ(p[0], 0x55); - EXPECT_EQ(p[1], 0x55); - EXPECT_EQ(p[2], 0b00000000); + EXPECT_EQ(p[0], 0x45); + EXPECT_EQ(p[1], 0x51); + EXPECT_EQ(p[2], 0x14); } TEST(constant, uint6_vector_less_than_one_storage_unit) { @@ -1208,9 +1203,9 @@ TEST(constant, uint6_vector_less_than_one_storage_unit) { EXPECT_THAT(v, ElementsAre(5, 23, 1)); const auto p = c.get_data_ptr(); - EXPECT_EQ(p[0], 0x57); - EXPECT_EQ(p[1], 0x10); - EXPECT_EQ(p[2], 0b00010000); + EXPECT_EQ(p[0], 0xc5); + EXPECT_EQ(p[1], 0x15); + EXPECT_EQ(p[2], 0); } TEST(constant, uint6_vector_greater_than_one_storage_unit) { @@ -1224,13 +1219,11 @@ TEST(constant, uint6_vector_greater_than_one_storage_unit) { EXPECT_THAT(v, ElementsAre(25, 3, 1, 0, 45, 5)); const auto p = c.get_vector(); - EXPECT_EQ(p[0], 0x93); + EXPECT_EQ(p[0], 0xd9); EXPECT_EQ(p[1], 0x10); - EXPECT_EQ(p[2], 0b01000000); - - EXPECT_EQ(p[3], 0xd5); - EXPECT_EQ(p[4], 0); - EXPECT_EQ(p[5], 0b10000000); + EXPECT_EQ(p[2], 0x00); + EXPECT_EQ(p[3], 0x6d); + EXPECT_EQ(p[4], 0x01); } TEST(constant, uint6_vector_broadcast) { @@ -1242,9 +1235,9 @@ TEST(constant, uint6_vector_broadcast) { EXPECT_THAT(v, Each(45)); const auto p = c.get_data_ptr(); - EXPECT_EQ(p[0], 0xdd); - EXPECT_EQ(p[1], 0xdd); - EXPECT_EQ(p[2], 0b10101010); + EXPECT_EQ(p[0], 0x6d); + EXPECT_EQ(p[1], 0xdb); + EXPECT_EQ(p[2], 0xb6); } TEST(constant, uint6_write_then_cast_custom_type) { diff --git a/src/core/tests/element_iterator_test.cpp b/src/core/tests/element_iterator_test.cpp index 0bf59ba0b9321a..e8b0f8d0054dbe 100644 --- a/src/core/tests/element_iterator_test.cpp +++ b/src/core/tests/element_iterator_test.cpp @@ -194,12 +194,17 @@ TEST(ElementIteratorTest, write_u3_data) { std::copy(input.begin(), input.end(), iter); - EXPECT_THAT(output, ElementsAre(0b10110001, 0b00011011, 0b00001111)); + EXPECT_THAT(output, ElementsAre(0x1a, 0xc2, 0xfa)); } TEST(ElementIteratorTest, read_non_const_u3_data) { constexpr auto elements_count = 16; - auto input = std::array{0x7a, 0x6f, 0x55, static_cast(0xb1), 0x1b, 0x0f}; + auto input = std::array{static_cast(0xb9), + 0x1c, + static_cast(0xef), + 0x1a, + static_cast(0xc2), + static_cast(0xfa)}; auto iter = element::iterator(input.data()); EXPECT_THAT(std::vector(iter, iter + elements_count), @@ -208,7 +213,8 @@ TEST(ElementIteratorTest, read_non_const_u3_data) { TEST(ElementIteratorTest, read_const_u3_data) { constexpr auto elements_count = 8; - constexpr auto input = std::array{static_cast(0b10110001), 0b00011011, 0b00001111}; + constexpr auto input = + std::array{0x1a, static_cast(0xc2), static_cast(0xfa)}; auto iter = element::iterator(input.data()); EXPECT_THAT(std::vector(iter, iter + elements_count), ElementsAre(2, 3, 0, 1, 4, 5, 6, 7)); @@ -216,7 +222,12 @@ TEST(ElementIteratorTest, read_const_u3_data) { TEST(ElementIteratorTest, read_u3_data_iterator_with_offset) { // Has values {1, 7, 2, 6, 1, 6, 3, 7, [2], 3, 0, 1, 4, 5, 6, 7} - auto input = std::array{0x7a, 0x6f, 0x55, static_cast(0xb1), 0x1b, 0x0f}; + auto input = std::array{static_cast(0xb9), + 0x1c, + static_cast(0xef), + 0x1a, + static_cast(0xc2), + static_cast(0xfa)}; auto iter = element::iterator(input.data() + 3); EXPECT_EQ(*iter, 2); @@ -234,7 +245,12 @@ TEST(ElementIteratorTest, read_u3_data_iterator_with_offset) { TEST(ElementIteratorTest, read_u3_from_tensor) { // Has values {1, 7, 2, 6, 1, 6, 3, 7, [2], 3, 0, 1, 4, 5, 6, 7} - auto input = std::array{0x7a, 0x6f, 0x55, static_cast(0xb1), 0x1b, 0x0f}; + auto input = std::array{static_cast(0xb9), + 0x1c, + static_cast(0xef), + 0x1a, + static_cast(0xc2), + static_cast(0xfa)}; auto t = ov::Tensor(element::u3, Shape{4, 2, 2}, input.data()); auto iter = element::iterator(static_cast(t.data(element::u3))); @@ -419,12 +435,12 @@ TEST(ElementIteratorTest, write_u6_data) { std::copy(input.begin(), input.end(), iter); - EXPECT_THAT(output, ElementsAre(0x21, 0x03, 0x00, 0x21, 0x30, 0x79)); + EXPECT_THAT(output, ElementsAre(0x42, 0x00, 0x0c, 0x52, 0x3c, 0x42)); } TEST(ElementIteratorTest, read_non_const_u6_data) { constexpr auto elements_count = 8; - auto input = std::array{0x21, 0x03, 0x00, 0x21, 0x30, 0x79}; + auto input = std::array{0x42, 0x00, 0x0c, 0x52, 0x3c, 0x42}; auto iter = element::iterator(input.data()); EXPECT_THAT(std::vector(iter, iter + elements_count), ElementsAre(2, 1, 0, 3, 18, 49, 35, 16)); @@ -432,7 +448,7 @@ TEST(ElementIteratorTest, read_non_const_u6_data) { TEST(ElementIteratorTest, read_const_u6_data) { constexpr auto elements_count = 8; - constexpr auto input = std::array{0x21, 0x03, 0x00, 0x21, 0x30, 0x79}; + constexpr auto input = std::array{0x42, 0x00, 0x0c, 0x52, 0x3c, 0x42}; auto iter = element::iterator(input.data()); EXPECT_THAT(std::vector(iter, iter + elements_count), ElementsAre(2, 1, 0, 3, 18, 49, 35, 16)); @@ -440,7 +456,7 @@ TEST(ElementIteratorTest, read_const_u6_data) { TEST(ElementIteratorTest, read_u6_data_increment_decrement_iterator) { // Has values {1, 2, 3, 10, [3], 8, 7, 2} - auto input = std::array{0x12, 0x3a, 0x00, 0x38, 0x72, 0x00}; + auto input = std::array{static_cast(0x81), 0x30, 0x28, 0x03, 0x72, 0x08}; auto iter = element::iterator(input.data() + 3); EXPECT_EQ(*iter--, 3); @@ -452,7 +468,15 @@ TEST(ElementIteratorTest, read_u6_data_increment_decrement_iterator) { TEST(ElementIteratorTest, read_u6_data_iterator_with_offset) { // Has values {1, 2, 3, 10, [3], 8, 7, 2, 1, 42, 4, 20} - auto input = std::array{0x12, 0x3a, 0x00, 0x38, 0x72, 0x00, 0x1a, 0x44, 0x21}; + auto input = std::array{static_cast(0x81), + 0x30, + 0x28, + 0x03, + 0x72, + 0x08, + static_cast(0x81), + 0x4a, + 0x50}; auto iter = element::iterator(input.data() + 3); EXPECT_EQ(*iter, 3); @@ -467,7 +491,7 @@ TEST(ElementIteratorTest, read_u6_data_iterator_with_offset) { } TEST(ElementIteratorTest, u6_value_to_output_stream) { - auto input = std::array{0x12, 0x3a, 0x00}; + auto input = std::array{0x01, 0x00, 0x00}; auto iter = element::iterator(input.data()); std::stringstream s; @@ -478,7 +502,15 @@ TEST(ElementIteratorTest, u6_value_to_output_stream) { TEST(ElementIteratorTest, read_u6_from_tensor) { // Has values {1, 2, 3, 10, 3, 8, 7, 2, 1, 42, 4, 20} - auto input = std::array{0x12, 0x3a, 0x00, 0x38, 0x72, 0x00, 0x1a, 0x44, 0x21}; + auto input = std::array{static_cast(0x81), + 0x30, + 0x28, + 0x03, + 0x72, + 0x08, + static_cast(0x81), + 0x4a, + 0x50}; auto t = ov::Tensor(element::u6, Shape{4, 1, 3}, input.data()); auto iter = element::iterator(static_cast(t.data(element::u6))); diff --git a/src/core/tests/memory_util.cpp b/src/core/tests/memory_util.cpp index 4708f7508b700d..8ed6aa2c1d2f2b 100644 --- a/src/core/tests/memory_util.cpp +++ b/src/core/tests/memory_util.cpp @@ -127,21 +127,21 @@ INSTANTIATE_TEST_SUITE_P(nibble_type_precision, INSTANTIATE_TEST_SUITE_P(split_bit_type_precision, GetMaxElementsForMemorySizeTest, testing::Values(std::make_tuple(element::u3, 0, 0), - std::make_tuple(element::u3, 1, 0), - std::make_tuple(element::u3, 2, 0), + std::make_tuple(element::u3, 1, 2), + std::make_tuple(element::u3, 2, 5), std::make_tuple(element::u3, 3, 8), - std::make_tuple(element::u3, 5, 8), + std::make_tuple(element::u3, 5, 13), std::make_tuple(element::u3, 6, 16), - std::make_tuple(element::u3, 11, 24), + std::make_tuple(element::u3, 11, 29), std::make_tuple(element::u3, 12, 32), std::make_tuple(element::u6, 0, 0), - std::make_tuple(element::u6, 1, 0), - std::make_tuple(element::u6, 2, 0), + std::make_tuple(element::u6, 1, 1), + std::make_tuple(element::u6, 2, 2), std::make_tuple(element::u6, 3, 4), - std::make_tuple(element::u6, 4, 4), - std::make_tuple(element::u6, 5, 4), + std::make_tuple(element::u6, 4, 5), + std::make_tuple(element::u6, 5, 6), std::make_tuple(element::u6, 6, 8), - std::make_tuple(element::u6, 11, 12), + std::make_tuple(element::u6, 11, 14), std::make_tuple(element::u6, 12, 16))); INSTANTIATE_TEST_SUITE_P(byte_type_precision, diff --git a/src/core/tests/ov_tensor_test.cpp b/src/core/tests/ov_tensor_test.cpp index 6dd2fa2e1dea19..2f281eb7ce7680 100644 --- a/src/core/tests/ov_tensor_test.cpp +++ b/src/core/tests/ov_tensor_test.cpp @@ -248,7 +248,7 @@ TEST_F(OVTensorTest, canCreateTensorU3UsingMockAllocator) { } TEST_F(OVTensorTest, canCreateTensorU6UsingMockAllocator) { - constexpr size_t exp_size = 6; + constexpr size_t exp_size = 5; ov::Shape shape = {1, 2, 3}; OVMockAllocator allocator; @@ -807,7 +807,7 @@ TEST_F(OVTensorTest, getByteSizeU2NotEvenDivByStorageUnit) { TEST_F(OVTensorTest, getByteSizeU3LessThanMinStorageUnit) { const auto tensor = ov::Tensor(ov::element::u3, ov::Shape{3}); - EXPECT_EQ(tensor.get_byte_size(), 3); + EXPECT_EQ(tensor.get_byte_size(), 2); } TEST_F(OVTensorTest, getByteSizeU3EvenDivByStorageUnit) { @@ -817,7 +817,7 @@ TEST_F(OVTensorTest, getByteSizeU3EvenDivByStorageUnit) { TEST_F(OVTensorTest, getByteSizeU3NotEvenDivByStorageUnit) { const auto tensor = ov::Tensor(ov::element::u3, ov::Shape{17}); - EXPECT_EQ(tensor.get_byte_size(), 3 + 2 * 3); + EXPECT_EQ(tensor.get_byte_size(), 7); } TEST_F(OVTensorTest, getByteSizeU6LessThanMinStorageUnit) { @@ -832,7 +832,7 @@ TEST_F(OVTensorTest, getByteSizeU6EvenDivByStorageUnit) { TEST_F(OVTensorTest, getByteSizeU6NotEvenDivByStorageUnit) { const auto tensor = ov::Tensor(ov::element::u6, ov::Shape{17}); - EXPECT_EQ(tensor.get_byte_size(), 3 + 4 * 3); + EXPECT_EQ(tensor.get_byte_size(), 13); } TEST_F(OVTensorTest, checkIsContinuousTensorScalar) { diff --git a/src/plugins/template/tests/functional/op_reference/transpose.cpp b/src/plugins/template/tests/functional/op_reference/transpose.cpp index 78b37dc5754c0c..6b7eacabba93ee 100644 --- a/src/plugins/template/tests/functional/op_reference/transpose.cpp +++ b/src/plugins/template/tests/functional/op_reference/transpose.cpp @@ -276,9 +276,10 @@ std::vector generateThrowingTransposeParams() { std::vector generateTransposeParamsForSubByte() { std::vector params; - // NOTE: Sub-byte types (u2, u4, i4) pack multiple values per byte. - // These tests validate transpose_2bit and transpose_4bit reference implementations. - // u2: 4 values per byte (2 bits each), u4/i4: 2 values per byte (4 bits each) + // NOTE: Sub-byte types (u2, u3, u4, u6, i4) pack multiple values per byte. + // These tests validate transpose_2bit, transpose_subbyte and transpose_4bit reference implementations. + // u2: 4 values per byte (2 bits each), u4/i4: 2 values per byte (4 bits each), + // u3/u6: linear LSB-first bit-stream, values may straddle a byte boundary. // u2 transpose test - swap dimensions // Input: [2,2] = [[0,1], [2,3]] with axes {1,0} @@ -290,6 +291,30 @@ std::vector generateTransposeParamsForSubByte() { reference_tests::Tensor(element::u2, {2, 2}, std::vector{0xD8}), // {0,2,1,3} "transpose_u2_2d_swap")); + // u3 transpose test - swap dimensions (linear LSB-first packed bit-stream) + // Input: [2,4] = [[2,3,0,1], [4,5,6,7]] with axes {1,0} + // Output: [4,2] = [[2,4], [3,5], [0,6], [1,7]] = {2,4,3,5,0,6,1,7} + params.push_back(TransposeParams( + PartialShape::dynamic(), + reference_tests::Tensor(element::u3, {2, 4}, std::vector{0x1a, 0xc2, 0xfa}), // {2,3,0,1,4,5,6,7} + reference_tests::Tensor(element::i64, {2}, std::vector{1, 0}), + reference_tests::Tensor(element::u3, {4, 2}, std::vector{0xe2, 0x0a, 0xe7}), // {2,4,3,5,0,6,1,7} + "transpose_u3_2d_swap")); + + // u6 transpose test - swap dimensions (linear LSB-first packed bit-stream) + // Input: [2,4] = [[2,3,0,1], [4,5,6,7]] with axes {1,0} + // Output: [4,2] = [[2,4], [3,5], [0,6], [1,7]] = {2,4,3,5,0,6,1,7} + params.push_back(TransposeParams( + PartialShape::dynamic(), + reference_tests::Tensor(element::u6, + {2, 4}, + std::vector{0xc2, 0x00, 0x04, 0x44, 0x61, 0x1c}), // {2,3,0,1,4,5,6,7} + reference_tests::Tensor(element::i64, {2}, std::vector{1, 0}), + reference_tests::Tensor(element::u6, + {4, 2}, + std::vector{0x02, 0x31, 0x14, 0x80, 0x11, 0x1c}), // {2,4,3,5,0,6,1,7} + "transpose_u6_2d_swap")); + // u4 transpose test - swap dimensions // Input: [2,2] = [[1,2], [3,4]] with axes {1,0} // Output: [2,2] = [[1,3], [2,4]] = {1,3,2,4}