@@ -612,7 +612,207 @@ 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 (
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
618818def average (x , axis = None , weights = None ):
0 commit comments