Skip to content

Commit f7bdc1c

Browse files
committed
Implement numpy.view for OpenVINO backend
1 parent d8e68b0 commit f7bdc1c

File tree

2 files changed

+201
-5
lines changed

2 files changed

+201
-5
lines changed

keras/src/backend/openvino/excluded_concrete_tests.txt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,6 @@ NumpyDtypeTest::test_nanargmax
323323
NumpyDtypeTest::test_nanargmin
324324
NumpyDtypeTest::test_power
325325
NumpyDtypeTest::test_sinc
326-
NumpyDtypeTest::test_view
327326
NumpyOneInputOpsCorrectnessTest::test_array
328327
NumpyOneInputOpsCorrectnessTest::test_conj
329328
NumpyOneInputOpsCorrectnessTest::test_imag
@@ -334,11 +333,8 @@ NumpyOneInputOpsCorrectnessTest::test_real
334333
NumpyOneInputOpsCorrectnessTest::test_reshape
335334
NumpyOneInputOpsCorrectnessTest::test_sinc
336335
NumpyOneInputOpsCorrectnessTest::test_vectorize
337-
NumpyOneInputOpsCorrectnessTest::test_view
338336
NumpyOneInputOpsDynamicShapeTest::test_sinc
339-
NumpyOneInputOpsDynamicShapeTest::test_view
340337
NumpyOneInputOpsStaticShapeTest::test_sinc
341-
NumpyOneInputOpsStaticShapeTest::test_view
342338
OptimizerTest::test_constraints_are_applied
343339
OptimizerTest::test_ema
344340
OptimizerTest::test_gradient_accumulation

keras/src/backend/openvino/numpy.py

Lines changed: 201 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -612,7 +612,207 @@ def array(x, dtype=None):
612612

613613

614614
def view(x, dtype=None):
615-
raise NotImplementedError("`view` is not supported with openvino backend")
615+
"""Reinterpret the bytes of a tensor as a different dtype.
616+
617+
Three execution paths:
618+
1. **NumPy fast path** — plain ``np.ndarray`` inputs are viewed
619+
directly and re-wrapped as an OpenVINO constant.
620+
2. **Constant folding** — if the OpenVINO subgraph backing *x*
621+
is parameter-free (e.g. a ``Constant`` node *or* an expression
622+
such as ``Broadcast``), it is compiled on CPU and the resulting
623+
NumPy array is viewed.
624+
3. **Symbolic bitwise decomposition** — for non-constant,
625+
integer-to-integer type changes the bit pattern is preserved
626+
using shift / mask / OR operations in the OpenVINO graph.
627+
Float ↔ int reinterpretation on symbolic tensors is not
628+
supported because OpenVINO lacks a bitcast op.
629+
"""
630+
from keras.src import backend
631+
632+
new_dtype = backend.standardize_dtype(dtype) if dtype else None
633+
634+
# Fast path: plain numpy/scalar inputs
635+
if isinstance(x, np.ndarray):
636+
if new_dtype is None:
637+
return OpenVINOKerasTensor(ov_opset.constant(x).output(0))
638+
return OpenVINOKerasTensor(
639+
ov_opset.constant(x.view(np.dtype(new_dtype))).output(0)
640+
)
641+
642+
x_ov = get_ov_output(x)
643+
old_ov_type = x_ov.get_element_type()
644+
old_dtype = ov_to_keras_type(old_ov_type)
645+
646+
if new_dtype is None:
647+
new_dtype = old_dtype
648+
new_ov_type = OPENVINO_DTYPES[new_dtype]
649+
650+
if old_ov_type == new_ov_type:
651+
return OpenVINOKerasTensor(x_ov)
652+
653+
old_itemsize = old_ov_type.size
654+
new_itemsize = new_ov_type.size
655+
656+
# Constant folding: evaluate parameter-free subgraphs on CPU.
657+
# Uses raw bytes + ov.Tensor to avoid numpy dtype issues
658+
# (e.g. bfloat16 is not a standard numpy dtype).
659+
try:
660+
node = x_ov.get_node()
661+
if node.get_type_name() == "Constant":
662+
np_data = node.data
663+
else:
664+
ov_model = ov.Model(results=[x_ov], parameters=[])
665+
compiled = ov.compile_model(ov_model, "CPU")
666+
np_data = compiled({})[0]
667+
old_shape = np_data.shape
668+
new_last = old_shape[-1] * old_itemsize // new_itemsize
669+
new_shape = list(old_shape[:-1]) + [new_last]
670+
raw = np.frombuffer(np_data.tobytes(), dtype=np.uint8)
671+
result_tensor = ov.Tensor(new_ov_type, new_shape)
672+
np.copyto(
673+
np.frombuffer(result_tensor.data, dtype=np.uint8),
674+
raw,
675+
)
676+
return OpenVINOKerasTensor(
677+
ov_opset.constant(result_tensor).output(0)
678+
)
679+
except Exception:
680+
pass
681+
682+
# Non-constant tensors: only integer↔integer is supported
683+
if not (old_ov_type.is_integral() and new_ov_type.is_integral()):
684+
raise NotImplementedError(
685+
f"`view` from {old_dtype} to {new_dtype} is not supported "
686+
"for non-constant tensors with the OpenVINO backend "
687+
"(no bitcast operation available in OpenVINO opset)."
688+
)
689+
690+
if old_itemsize == new_itemsize:
691+
# Same-width signed↔unsigned: convert preserves bit pattern
692+
return OpenVINOKerasTensor(
693+
ov_opset.convert(x_ov, new_ov_type).output(0)
694+
)
695+
elif old_itemsize > new_itemsize:
696+
return _view_int_expand(
697+
x_ov, new_ov_type, old_itemsize, new_itemsize
698+
)
699+
else:
700+
return _view_int_contract(
701+
x_ov, new_ov_type, old_itemsize, new_itemsize
702+
)
703+
704+
705+
def _split_shape_leading_last(x):
706+
"""Return (leading_dims, last_dim) from the shape of *x*.
707+
708+
``leading_dims`` contains all dimensions except the last one,
709+
``last_dim`` is a rank-1 tensor with the single last dimension.
710+
Both are ``i64`` OpenVINO outputs.
711+
"""
712+
shape = ov_opset.shape_of(x, "i64").output(0)
713+
leading = ov_opset.slice(
714+
shape,
715+
ov_opset.constant([0], Type.i64).output(0),
716+
ov_opset.constant([-1], Type.i64).output(0),
717+
ov_opset.constant([1], Type.i64).output(0),
718+
ov_opset.constant([0], Type.i64).output(0),
719+
).output(0)
720+
last_dim = ov_opset.gather(
721+
shape,
722+
ov_opset.constant([-1], Type.i64).output(0),
723+
ov_opset.constant(0, Type.i64).output(0),
724+
).output(0)
725+
return leading, last_dim
726+
727+
728+
def _view_int_expand(x, new_ov_type, old_itemsize, new_itemsize):
729+
"""View a larger int as smaller ints (e.g. int32 → uint8)."""
730+
ratio = old_itemsize // new_itemsize
731+
_unsigned = {1: Type.u8, 2: Type.u16, 4: Type.u32, 8: Type.u64}
732+
src_uint_type = _unsigned[old_itemsize]
733+
dst_uint_type = _unsigned[new_itemsize]
734+
bits_per_elem = new_itemsize * 8
735+
mask_val = (1 << bits_per_elem) - 1
736+
737+
x_uint = ov_opset.convert(x, src_uint_type).output(0)
738+
mask = ov_opset.constant(mask_val, src_uint_type).output(0)
739+
740+
byte_parts = []
741+
for i in range(ratio):
742+
shift = ov_opset.constant(
743+
i * bits_per_elem, src_uint_type
744+
).output(0)
745+
shifted = ov_opset.bitwise_right_shift(
746+
x_uint, shift
747+
).output(0)
748+
masked = ov_opset.bitwise_and(shifted, mask).output(0)
749+
part = ov_opset.convert(masked, dst_uint_type).output(0)
750+
part = ov_opset.unsqueeze(
751+
part, ov_opset.constant(-1, Type.i32)
752+
).output(0)
753+
byte_parts.append(part)
754+
755+
# Concat along last axis: [..., N, ratio]
756+
concat_result = ov_opset.concat(byte_parts, axis=-1).output(0)
757+
758+
# Reshape [..., N, ratio] → [..., N*ratio]
759+
leading, last_dim = _split_shape_leading_last(x)
760+
new_last = ov_opset.multiply(
761+
last_dim, ov_opset.constant([ratio], Type.i64).output(0)
762+
).output(0)
763+
new_shape = ov_opset.concat([leading, new_last], axis=0).output(0)
764+
result = ov_opset.reshape(concat_result, new_shape, False).output(0)
765+
766+
if dst_uint_type != new_ov_type:
767+
result = ov_opset.convert(result, new_ov_type).output(0)
768+
return OpenVINOKerasTensor(result)
769+
770+
771+
def _view_int_contract(x, new_ov_type, old_itemsize, new_itemsize):
772+
"""View smaller ints as a larger int (e.g. uint8 → int32)."""
773+
ratio = new_itemsize // old_itemsize
774+
_unsigned = {1: Type.u8, 2: Type.u16, 4: Type.u32, 8: Type.u64}
775+
src_uint_type = _unsigned[old_itemsize]
776+
dst_uint_type = _unsigned[new_itemsize]
777+
bits_per_elem = old_itemsize * 8
778+
779+
x_uint = ov_opset.convert(x, src_uint_type).output(0)
780+
781+
# Reshape [..., N] → [..., N//ratio, ratio]
782+
leading, last_dim = _split_shape_leading_last(x)
783+
grouped_last = ov_opset.divide(
784+
last_dim, ov_opset.constant([ratio], Type.i64).output(0)
785+
).output(0)
786+
ratio_dim = ov_opset.constant([ratio], Type.i64).output(0)
787+
inter_shape = ov_opset.concat(
788+
[leading, grouped_last, ratio_dim], axis=0
789+
).output(0)
790+
reshaped = ov_opset.reshape(x_uint, inter_shape, False).output(0)
791+
792+
# Combine bytes: gather each position, shift, OR
793+
last_axis = ov_opset.constant(-1, Type.i64).output(0)
794+
idx = ov_opset.constant([0], Type.i64).output(0)
795+
byte_0 = ov_opset.gather(reshaped, idx, last_axis).output(0)
796+
byte_0 = ov_opset.squeeze(
797+
byte_0, ov_opset.constant([-1], Type.i32)
798+
).output(0)
799+
result = ov_opset.convert(byte_0, dst_uint_type).output(0)
800+
for i in range(1, ratio):
801+
idx = ov_opset.constant([i], Type.i64).output(0)
802+
byte_i = ov_opset.gather(reshaped, idx, last_axis).output(0)
803+
byte_i = ov_opset.squeeze(
804+
byte_i, ov_opset.constant([-1], Type.i32)
805+
).output(0)
806+
byte_i = ov_opset.convert(byte_i, dst_uint_type).output(0)
807+
shift = ov_opset.constant(
808+
i * bits_per_elem, dst_uint_type
809+
).output(0)
810+
byte_i = ov_opset.bitwise_left_shift(byte_i, shift).output(0)
811+
result = ov_opset.bitwise_or(result, byte_i).output(0)
812+
813+
if dst_uint_type != new_ov_type:
814+
result = ov_opset.convert(result, new_ov_type).output(0)
815+
return OpenVINOKerasTensor(result)
616816

617817

618818
def average(x, axis=None, weights=None):

0 commit comments

Comments
 (0)