@@ -612,7 +612,195 @@ def array(x, dtype=None):
612612
613613
614614def 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 (ov_opset .constant (result_tensor ).output (0 ))
677+ except Exception :
678+ pass
679+
680+ # Non-constant tensors: only integer↔integer is supported
681+ if not (old_ov_type .is_integral () and new_ov_type .is_integral ()):
682+ raise NotImplementedError (
683+ f"`view` from { old_dtype } to { new_dtype } is not supported "
684+ "for non-constant tensors with the OpenVINO backend "
685+ "(no bitcast operation available in OpenVINO opset)."
686+ )
687+
688+ if old_itemsize == new_itemsize :
689+ # Same-width signed↔unsigned: convert preserves bit pattern
690+ return OpenVINOKerasTensor (
691+ ov_opset .convert (x_ov , new_ov_type ).output (0 )
692+ )
693+ elif old_itemsize > new_itemsize :
694+ return _view_int_expand (x_ov , new_ov_type , old_itemsize , new_itemsize )
695+ else :
696+ return _view_int_contract (x_ov , new_ov_type , old_itemsize , new_itemsize )
697+
698+
699+ def _split_shape_leading_last (x ):
700+ """Return (leading_dims, last_dim) from the shape of *x*.
701+
702+ ``leading_dims`` contains all dimensions except the last one,
703+ ``last_dim`` is a rank-1 tensor with the single last dimension.
704+ Both are ``i64`` OpenVINO outputs.
705+ """
706+ shape = ov_opset .shape_of (x , "i64" ).output (0 )
707+ leading = ov_opset .slice (
708+ shape ,
709+ ov_opset .constant ([0 ], Type .i64 ).output (0 ),
710+ ov_opset .constant ([- 1 ], Type .i64 ).output (0 ),
711+ ov_opset .constant ([1 ], Type .i64 ).output (0 ),
712+ ov_opset .constant ([0 ], Type .i64 ).output (0 ),
713+ ).output (0 )
714+ last_dim = ov_opset .gather (
715+ shape ,
716+ ov_opset .constant ([- 1 ], Type .i64 ).output (0 ),
717+ ov_opset .constant (0 , Type .i64 ).output (0 ),
718+ ).output (0 )
719+ return leading , last_dim
720+
721+
722+ def _view_int_expand (x , new_ov_type , old_itemsize , new_itemsize ):
723+ """View a larger int as smaller ints (e.g. int32 → uint8)."""
724+ ratio = old_itemsize // new_itemsize
725+ _unsigned = {1 : Type .u8 , 2 : Type .u16 , 4 : Type .u32 , 8 : Type .u64 }
726+ src_uint_type = _unsigned [old_itemsize ]
727+ dst_uint_type = _unsigned [new_itemsize ]
728+ bits_per_elem = new_itemsize * 8
729+ mask_val = (1 << bits_per_elem ) - 1
730+
731+ x_uint = ov_opset .convert (x , src_uint_type ).output (0 )
732+ mask = ov_opset .constant (mask_val , src_uint_type ).output (0 )
733+
734+ byte_parts = []
735+ for i in range (ratio ):
736+ shift = ov_opset .constant (i * bits_per_elem , src_uint_type ).output (0 )
737+ shifted = ov_opset .bitwise_right_shift (x_uint , shift ).output (0 )
738+ masked = ov_opset .bitwise_and (shifted , mask ).output (0 )
739+ part = ov_opset .convert (masked , dst_uint_type ).output (0 )
740+ part = ov_opset .unsqueeze (part , ov_opset .constant (- 1 , Type .i32 )).output (
741+ 0
742+ )
743+ byte_parts .append (part )
744+
745+ # Concat along last axis: [..., N, ratio]
746+ concat_result = ov_opset .concat (byte_parts , axis = - 1 ).output (0 )
747+
748+ # Reshape [..., N, ratio] → [..., N*ratio]
749+ leading , last_dim = _split_shape_leading_last (x )
750+ new_last = ov_opset .multiply (
751+ last_dim , ov_opset .constant ([ratio ], Type .i64 ).output (0 )
752+ ).output (0 )
753+ new_shape = ov_opset .concat ([leading , new_last ], axis = 0 ).output (0 )
754+ result = ov_opset .reshape (concat_result , new_shape , False ).output (0 )
755+
756+ if dst_uint_type != new_ov_type :
757+ result = ov_opset .convert (result , new_ov_type ).output (0 )
758+ return OpenVINOKerasTensor (result )
759+
760+
761+ def _view_int_contract (x , new_ov_type , old_itemsize , new_itemsize ):
762+ """View smaller ints as a larger int (e.g. uint8 → int32)."""
763+ ratio = new_itemsize // old_itemsize
764+ _unsigned = {1 : Type .u8 , 2 : Type .u16 , 4 : Type .u32 , 8 : Type .u64 }
765+ src_uint_type = _unsigned [old_itemsize ]
766+ dst_uint_type = _unsigned [new_itemsize ]
767+ bits_per_elem = old_itemsize * 8
768+
769+ x_uint = ov_opset .convert (x , src_uint_type ).output (0 )
770+
771+ # Reshape [..., N] → [..., N//ratio, ratio]
772+ leading , last_dim = _split_shape_leading_last (x )
773+ grouped_last = ov_opset .divide (
774+ last_dim , ov_opset .constant ([ratio ], Type .i64 ).output (0 )
775+ ).output (0 )
776+ ratio_dim = ov_opset .constant ([ratio ], Type .i64 ).output (0 )
777+ inter_shape = ov_opset .concat (
778+ [leading , grouped_last , ratio_dim ], axis = 0
779+ ).output (0 )
780+ reshaped = ov_opset .reshape (x_uint , inter_shape , False ).output (0 )
781+
782+ # Combine bytes: gather each position, shift, OR
783+ last_axis = ov_opset .constant (- 1 , Type .i64 ).output (0 )
784+ idx = ov_opset .constant ([0 ], Type .i64 ).output (0 )
785+ byte_0 = ov_opset .gather (reshaped , idx , last_axis ).output (0 )
786+ byte_0 = ov_opset .squeeze (byte_0 , ov_opset .constant ([- 1 ], Type .i32 )).output (
787+ 0
788+ )
789+ result = ov_opset .convert (byte_0 , dst_uint_type ).output (0 )
790+ for i in range (1 , ratio ):
791+ idx = ov_opset .constant ([i ], Type .i64 ).output (0 )
792+ byte_i = ov_opset .gather (reshaped , idx , last_axis ).output (0 )
793+ byte_i = ov_opset .squeeze (
794+ byte_i , ov_opset .constant ([- 1 ], Type .i32 )
795+ ).output (0 )
796+ byte_i = ov_opset .convert (byte_i , dst_uint_type ).output (0 )
797+ shift = ov_opset .constant (i * bits_per_elem , dst_uint_type ).output (0 )
798+ byte_i = ov_opset .bitwise_left_shift (byte_i , shift ).output (0 )
799+ result = ov_opset .bitwise_or (result , byte_i ).output (0 )
800+
801+ if dst_uint_type != new_ov_type :
802+ result = ov_opset .convert (result , new_ov_type ).output (0 )
803+ return OpenVINOKerasTensor (result )
616804
617805
618806def average (x , axis = None , weights = None ):
0 commit comments