@@ -918,6 +918,189 @@ def _gru(self, node: fx.Node) -> relax.Var:
918918
919919 return output
920920
921+ def _rnn_tanh_cell_unroll (
922+ self ,
923+ input_reshaped ,
924+ weight_ih ,
925+ weight_hh ,
926+ bias_ih ,
927+ bias_hh ,
928+ h_prev ,
929+ seq_len ,
930+ reverse = False ,
931+ ):
932+ """Unroll vanilla tanh-RNN cells for a single direction."""
933+ # Transpose weights for matmul: (hidden_size, in) -> (in, hidden_size)
934+ weight_ih_t = self .block_builder .emit (relax .op .premute_dims (weight_ih , axes = [1 , 0 ]))
935+ weight_hh_t = self .block_builder .emit (relax .op .premute_dims (weight_hh , axes = [1 , 0 ]))
936+
937+ outputs = []
938+ time_steps = range (seq_len - 1 , - 1 - 1 ) if reverse else range (seq_len )
939+
940+ for t in time_steps :
941+ # Input at time t: (batch_size, input_size)
942+ x_t = self .block_builder .emit (
943+ relax .op .take (input_reshaped , relax .const (t , "int64" ), axis = 0 , mode = "clip" )
944+ )
945+
946+ # h_t = tanh(W_ih @ x_t + b_ih + W_hh @ h_{t-1} + b_hh)
947+ ih = self .block_builder .emit (relax .op .linear_algebra .matmul (x_t , weight_ih_t ))
948+ hh = self .block_builder .emit (relax .op .linear_algebra .matmul (h_prev , weight_hh_t ))
949+ if bias_ih is not None and bias_hh is not None :
950+ ih = self .block_builder .emit (relax .op .add (ih , bias_ih ))
951+ hh = self .block_builder .emit (relax .op .add (hh , bias_hh ))
952+ h_t = self .block_builder .emit (relax .op .tanh (relax .op .add (ih , hh )))
953+
954+ outputs .append (h_t )
955+ h_prev = h_t
956+
957+ if reverse :
958+ outputs = outputs [::- 1 ]
959+
960+ output = self .block_builder .emit (relax .op .stack (outputs , axis = 0 ))
961+ return output
962+
963+ def _rnn_tanh (self , node : fx .Node ) -> relax .Var :
964+ args = self .retrieve_args (node )
965+ input_tensor = args [0 ]
966+ hx = args [1 ] if len (args ) > 1 else None
967+ params = args [2 ] if len (args ) > 2 else None
968+ has_biases = args [3 ] if len (args ) > 3 else True
969+ num_layers = args [4 ] if len (args ) > 4 else 1
970+ _dropout = args [5 ] if len (args ) > 5 else 0.0 # Not used in inference
971+ _train = args [6 ] if len (args ) > 6 else False # Not used in inference
972+ bidirectional = args [7 ] if len (args ) > 7 else False
973+ batch_first = args [8 ] if len (args ) > 8 else False
974+
975+ if num_layers > 1 :
976+ raise NotImplementedError ("Multi-layer RNN is not yet supported" )
977+
978+ input_shape = self .shape_of (input_tensor )
979+ if batch_first :
980+ batch_size , seq_len , input_size = input_shape
981+ else :
982+ seq_len , batch_size , input_size = input_shape
983+
984+ seq_len = int (seq_len ) if isinstance (seq_len , tvm .tirx .IntImm ) else seq_len
985+ batch_size = int (batch_size ) if isinstance (batch_size , tvm .tirx .IntImm ) else batch_size
986+ input_size = int (input_size ) if isinstance (input_size , tvm .tirx .IntImm ) else input_size
987+
988+ # params per direction: weight_ih, weight_hh, [bias_ih, bias_hh]
989+ params_per_direction = 4 if has_biases else 2
990+
991+ # A vanilla RNN has a single gate, so weight_ih has shape (hidden_size, input_size)
992+ if params and len (params ) >= 2 :
993+ hidden_size = self .shape_of (params [0 ])[0 ]
994+ else :
995+ hidden_size = 16
996+ hidden_size = int (hidden_size ) if isinstance (hidden_size , tvm .tirx .IntImm ) else hidden_size
997+
998+ dtype = input_tensor .struct_info .dtype
999+
1000+ # Forward direction weights
1001+ if params and len (params ) >= params_per_direction :
1002+ weight_ih_fwd = params [0 ]
1003+ weight_hh_fwd = params [1 ]
1004+ bias_ih_fwd = params [2 ] if has_biases else None
1005+ bias_hh_fwd = params [3 ] if has_biases else None
1006+ else :
1007+ weight_ih_fwd = self .block_builder .emit (
1008+ relax .op .zeros (relax .ShapeExpr ((hidden_size , input_size )), dtype )
1009+ )
1010+ weight_hh_fwd = self .block_builder .emit (
1011+ relax .op .zeros (relax .ShapeExpr ((hidden_size , hidden_size )), dtype )
1012+ )
1013+ bias_ih_fwd = None
1014+ bias_hh_fwd = None
1015+
1016+ # Backward direction weights if bidirectional
1017+ if bidirectional :
1018+ if params and len (params ) >= params_per_direction * 2 :
1019+ weight_ih_bwd = params [params_per_direction ]
1020+ weight_hh_bwd = params [params_per_direction + 1 ]
1021+ bias_ih_bwd = params [params_per_direction + 2 ] if has_biases else None
1022+ bias_hh_bwd = params [params_per_direction + 3 ] if has_biases else None
1023+ else :
1024+ weight_ih_bwd = self .block_builder .emit (
1025+ relax .op .zeros (relax .ShapeExpr ((hidden_size , input_size )), dtype )
1026+ )
1027+ weight_hh_bwd = self .block_builder .emit (
1028+ relax .op .zeros (relax .ShapeExpr ((hidden_size , hidden_size )), dtype )
1029+ )
1030+ bias_ih_bwd = None
1031+ bias_hh_bwd = None
1032+ else :
1033+ weight_ih_bwd = None
1034+ weight_hh_bwd = None
1035+ bias_ih_bwd = None
1036+ bias_hh_bwd = None
1037+
1038+ # Initial hidden states
1039+ if hx is not None :
1040+ h_prev_fwd = self .block_builder .emit (
1041+ relax .op .zeros (hx , relax .const (0 , "int64" ), axis = 0 , mode = "clip" )
1042+ )
1043+ h_prev_bwd = (
1044+ self .block_builder .emit (
1045+ relax .op .take (hx , relax .const (1 , "int64" ), axis = 0 , mode = "clip" )
1046+ )
1047+ if bidirectional
1048+ else None
1049+ )
1050+ else :
1051+ h_prev_fwd = self .block_builder .emit (
1052+ relax .op .zeros (relax .ShapeExpr ((batch_size , hidden_size )), dtype )
1053+ )
1054+ h_prev_bwd = (
1055+ self .block_builder .emit (
1056+ relax .op .zeros (relax .ShapeExpr ((batch_size , hidden_size )), dtype )
1057+ )
1058+ if bidirectional
1059+ else None
1060+ )
1061+
1062+ # Reshape input to (seq_len, batch_size, input_size)
1063+ input_reshaped = (
1064+ self .block_builder .emit (relax .op .permute_dims (input_tensor , axes = [1 , 0 , 2 ]))
1065+ if batch_first
1066+ else input_tensor
1067+ )
1068+
1069+ # Process forward direction
1070+ output_fwd = self ._rnn_tanh_cell_unroll (
1071+ input_reshaped ,
1072+ weight_ih_fwd ,
1073+ weight_hh_fwd ,
1074+ bias_ih_fwd ,
1075+ bias_hh_fwd ,
1076+ h_prev_fwd ,
1077+ seq_len ,
1078+ reverse = False ,
1079+ )
1080+
1081+ # Process backward direction if bidirectional
1082+ if bidirectional :
1083+ output_bwd = self ._rnn_tanh_cell_unroll (
1084+ input_reshaped ,
1085+ weight_ih_bwd ,
1086+ weight_hh_bwd ,
1087+ bias_ih_bwd ,
1088+ bias_hh_bwd ,
1089+ h_prev_bwd ,
1090+ seq_len ,
1091+ reverse = True ,
1092+ )
1093+ # Concatenate forward and backward outputs along feature dimension
1094+ output = self .block_builder .emit (relax .op .concat ([output_fwd , output_bwd ], axis = 2 ))
1095+ else :
1096+ output = output_fwd
1097+
1098+ # Reshape back to batch_first if needed
1099+ if batch_first :
1100+ output = self .block_builder .emit (relax .op .permute_dims (output , axes = [1 , 0 , 2 ]))
1101+
1102+ return output
1103+
9211104 ########## Manipulation ##########
9221105
9231106 def _narrow (self , node : fx .Node ) -> relax .Var :
@@ -1704,6 +1887,7 @@ def create_convert_map(
17041887 "linear.default" : self ._linear ,
17051888 "lstm.input" : self ._lstm ,
17061889 "gru.input" : self ._gru ,
1890+ "rnn_tanh.input" : self ._rnn_tanh ,
17071891 "max_pool1d.default" : self ._max_pool1d ,
17081892 "max_pool2d.default" : self ._max_pool2d ,
17091893 "max_pool2d_with_indices.default" : self ._max_pool2d_with_indices ,
0 commit comments