1# Copyright 2016 The TensorFlow Authors. All Rights Reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14# ============================================================================== 15"""Cudnn RNN operators.""" 16from __future__ import absolute_import 17from __future__ import division 18from __future__ import print_function 19 20from tensorflow.contrib.cudnn_rnn.python.ops import cudnn_rnn_ops 21from tensorflow.python.framework import dtypes 22from tensorflow.python.framework import ops 23from tensorflow.python.framework import tensor_shape 24from tensorflow.python.keras.engine import input_spec 25from tensorflow.python.layers import base as base_layer 26from tensorflow.python.ops import array_ops 27from tensorflow.python.ops import init_ops 28from tensorflow.python.ops import variable_scope as vs 29from tensorflow.python.platform import tf_logging as logging 30 31 32CUDNN_RNN_UNIDIRECTION = cudnn_rnn_ops.CUDNN_RNN_UNIDIRECTION 33CUDNN_RNN_BIDIRECTION = cudnn_rnn_ops.CUDNN_RNN_BIDIRECTION 34CUDNN_LSTM = cudnn_rnn_ops.CUDNN_LSTM 35CUDNN_GRU = cudnn_rnn_ops.CUDNN_GRU 36CUDNN_RNN_RELU = cudnn_rnn_ops.CUDNN_RNN_RELU 37CUDNN_RNN_TANH = cudnn_rnn_ops.CUDNN_RNN_TANH 38 39# Half for cell input, half for hidden states. 40CUDNN_LSTM_PARAMS_PER_LAYER = cudnn_rnn_ops.CUDNN_LSTM_PARAMS_PER_LAYER 41CUDNN_GRU_PARAMS_PER_LAYER = cudnn_rnn_ops.CUDNN_GRU_PARAMS_PER_LAYER 42CUDNN_RNN_TANH_PARAMS_PER_LAYER = cudnn_rnn_ops.CUDNN_RNN_TANH_PARAMS_PER_LAYER 43CUDNN_RNN_RELU_PARAMS_PER_LAYER = cudnn_rnn_ops.CUDNN_RNN_RELU_PARAMS_PER_LAYER 44 45CUDNN_INPUT_LINEAR_MODE = cudnn_rnn_ops.CUDNN_INPUT_LINEAR_MODE 46CUDNN_INPUT_SKIP_MODE = cudnn_rnn_ops.CUDNN_INPUT_SKIP_MODE 47CUDNN_INPUT_AUTO_MODE = cudnn_rnn_ops.CUDNN_INPUT_AUTO_MODE 48 49 50__all__ = ["CudnnLSTM", "CudnnGRU", "CudnnRNNTanh", "CudnnRNNRelu"] 51 52 53class _CudnnRNN(base_layer.Layer): 54 # pylint:disable=line-too-long 55 """Abstract class for RNN layers with Cudnn implementation. 56 57 Cudnn RNNs have two major differences from other platform-independent RNNs tf 58 provides: 59 * Cudnn LSTM and GRU are mathematically different from their tf counterparts. 60 (e.g. `tf.contrib.rnn.LSTMBlockCell` and `tf.nn.rnn_cell.GRUCell`. 61 * Cudnn-trained checkpoints are not directly compatible with tf RNNs: 62 * They use a single opaque parameter buffer for the entire (possibly) 63 multi-layer multi-directional RNN; Whereas tf RNN weights are per-cell and 64 layer. 65 * The size and layout of the parameter buffers may change between 66 CUDA/CuDNN/GPU generations. Because of that, the opaque parameter variable 67 does not have a static shape and is not partitionable. Instead of using 68 partitioning to alleviate the PS's traffic load, try building a 69 multi-tower model and do gradient aggregation locally within the host 70 before updating the PS. See https://www.tensorflow.org/performance/performance_models#parameter_server_variables 71 for a detailed performance guide. 72 73 Consequently, if one plans to use Cudnn trained models on both GPU and CPU 74 for inference and training, one needs to: 75 * Create a CudnnOpaqueParamsSaveable subclass object to save RNN params in 76 canonical format. (This is done for you automatically during layer building 77 process.) 78 * When not using a Cudnn RNN class, use CudnnCompatibleRNN classes to load the 79 checkpoints. These classes are platform-independent and perform the same 80 computation as Cudnn for training and inference. 81 Similarly, CudnnCompatibleRNN-trained checkpoints can be loaded by CudnnRNN 82 classes seamlessly. 83 84 Below is a typical workflow(using LSTM as an example): 85 for detailed performance guide. 86 87 # Use Cudnn-trained checkpoints with CudnnCompatibleRNNs 88 ```python 89 with tf.Graph().as_default(): 90 lstm = CudnnLSTM(num_layers, num_units, direction, ...) 91 92 outputs, output_states = lstm(inputs, initial_states, training=True) 93 94 # If user plans to delay calling the cell with inputs, one can do 95 # lstm.build(input_shape) 96 97 saver = Saver() 98 99 # training subgraph 100 ... 101 102 # Once in a while save the model. 103 saver.save(save_path) 104 105 # Inference subgraph for unidirectional RNN on, e.g., CPU or mobile. 106 with tf.Graph().as_default(): 107 single_cell = lambda: tf.contrib.cudnn_rnn.CudnnCompatibleLSTMCell(num_units) 108 109 # NOTE: Even if there's only one layer, the cell needs to be wrapped in 110 # MultiRNNCell. 111 cell = tf.nn.rnn_cell.MultiRNNCell( 112 [single_cell() for _ in range(num_layers)]) 113 114 # Leave the scope arg unset. 115 outputs, final_state = tf.nn.dynamic_rnn(cell, inputs, initial_state, ...) 116 117 saver = Saver() 118 119 # Create session 120 sess = ... 121 122 # Restores 123 saver.restore(sess, save_path) 124 125 # Inference subgraph for bidirectional RNN 126 with tf.Graph().as_default(): 127 single_cell = lambda: tf.contrib.cudnn_rnn.CudnnCompatibleLSTMCell(num_units) 128 cells_fw = [single_cell() for _ in range(num_layers)] 129 cells_bw = [single_cell() for _ in range(num_layers)] 130 131 # Leave the scope arg unset. 132 (outputs, output_state_fw, 133 output_state_bw) = tf.contrib.rnn.stack_bidirectional_dynamic_rnn( 134 cells_fw, cells_bw, inputs, ...) 135 saver = Saver() 136 137 # Create session 138 sess = ... 139 140 # Restores 141 saver.restore(sess, save_path) 142 ``` 143 """ 144 # pylint:enable=line-too-long 145 146 # TODO(allenl): Document object-based saving and checkpoint compatibility once 147 # it's implemented for more cuDNN Layers. 148 149 # The following are constants defined by subclasses. 150 # Type of RNN cell. 151 _rnn_mode = None 152 # Number of cell weights(or biases) per layer. 153 _num_params_per_layer = None 154 # Custom SaveableObject class for the CudnnRNN class. 155 _saveable_cls = None 156 157 def __init__(self, 158 num_layers, 159 num_units, 160 input_mode=CUDNN_INPUT_LINEAR_MODE, 161 direction=CUDNN_RNN_UNIDIRECTION, 162 dropout=0., 163 seed=None, 164 dtype=dtypes.float32, 165 kernel_initializer=None, 166 bias_initializer=None, 167 name=None): 168 """Creates a CudnnRNN model from model spec. 169 170 Args: 171 num_layers: the number of layers for the RNN model. 172 num_units: the number of units within the RNN model. 173 input_mode: indicate whether there is a linear projection between the 174 input and the actual computation before the first layer. It can be 175 'linear_input', 'skip_input' or 'auto_select'. 176 'linear_input' (default) always applies a linear projection of input 177 onto RNN hidden state. (standard RNN behavior). 178 'skip_input' is only allowed when input_size == num_units; 179 'auto_select' implies 'skip_input' when input_size == num_units; 180 otherwise, it implies 'linear_input'. 181 direction: the direction model that the model operates. Can be either 182 'unidirectional' or 'bidirectional' 183 dropout: dropout rate, a number between [0, 1]. Dropout is applied between 184 each layer (no dropout is applied for a model with a single layer). 185 When set to 0, dropout is disabled. 186 seed: the op seed used for initializing dropout. See `tf.set_random_seed` 187 for behavior. 188 dtype: tf.float16, tf.float32 or tf.float64 189 kernel_initializer: starting value to initialize the weight. 190 bias_initializer: starting value to initialize the bias 191 (default is all zeros). 192 name: VariableScope for the created subgraph; defaults to class name. 193 This only serves the default scope if later no scope is specified when 194 invoking __call__(). 195 196 Raises: 197 ValueError: if direction is invalid. Or dtype is not supported. 198 """ 199 super(_CudnnRNN, self).__init__(dtype=dtype, name=name) 200 cudnn_rnn_ops.check_direction(direction) 201 cudnn_rnn_ops.check_input_mode(input_mode) 202 203 if dtype not in [dtypes.float16, dtypes.float32, dtypes.float64]: 204 raise ValueError( 205 "Only support float16, float32, float64, provided %s" % dtype) 206 # Layer self.dtype is type name, the original DType object is kept here. 207 self._plain_dtype = dtype 208 self._num_layers = num_layers 209 self._num_units = num_units 210 self._input_mode = input_mode 211 self._direction = direction 212 self._dropout = dropout 213 self._seed = seed 214 self._kernel_initializer = kernel_initializer 215 self._bias_initializer = bias_initializer 216 # Init input_size to None, which will be set after build(). 217 self._input_size = None 218 self._saveable = None 219 220 @property 221 def num_layers(self): 222 return self._num_layers 223 224 @property 225 def num_units(self): 226 return self._num_units 227 228 @property 229 def input_mode(self): 230 """Input mode of first layer. 231 232 Indicates whether there is a linear projection between the input and the 233 actual computation before the first layer. It can be 234 * 'linear_input': (default) always applies a linear projection of input 235 onto RNN hidden state. (standard RNN behavior) 236 * 'skip_input': 'skip_input' is only allowed when input_size == num_units. 237 * 'auto_select'. implies 'skip_input' when input_size == num_units; 238 otherwise, it implies 'linear_input'. 239 240 Returns: 241 'linear_input', 'skip_input' or 'auto_select'. 242 """ 243 return self._input_mode 244 245 @property 246 def input_size(self): 247 if not self._input_size: 248 raise ValueError( 249 "\'input_size\' is unknown since layer has not been built.") 250 return self._input_size 251 252 @property 253 def rnn_mode(self): 254 """Type of RNN cell used. 255 256 Returns: 257 `lstm`, `gru`, `rnn_relu` or `rnn_tanh`. 258 """ 259 return self._rnn_mode 260 261 @property 262 def direction(self): 263 """Returns `unidirectional` or `bidirectional`.""" 264 return self._direction 265 266 @property 267 def num_dirs(self): 268 return 1 if self._direction == CUDNN_RNN_UNIDIRECTION else 2 269 270 @property 271 def saveable(self): 272 return self._saveable 273 274 @property 275 def canonical_weight_shapes(self): 276 """Shapes of Cudnn canonical weight tensors.""" 277 if not self._input_size: 278 raise RuntimeError( 279 "%s.canonical_weight_shapes invoked before input shape is known" % 280 type(self).__name__) 281 282 shapes = [] 283 for i in range(self._num_layers): 284 shapes.extend(self._canonical_weight_shape(i)) 285 return shapes 286 287 @property 288 def canonical_bias_shapes(self): 289 """Shapes of Cudnn canonical bias tensors.""" 290 return self._canonical_bias_shape(0) * self._num_layers 291 292 def _update_trainable_weights(self, getter, *args, **kwargs): 293 """Custom getter for layer variables.""" 294 # Add variables to layer's `(non_)trainable_weights` list(s). 295 variable = getter(*args, **kwargs) 296 trainable = kwargs.get("trainable", True) 297 if trainable and variable not in self._trainable_weights: 298 self._trainable_weights.append(variable) 299 elif not trainable and variable not in self._non_trainable_weights: 300 self._non_trainable_weights.append(variable) 301 return variable 302 303 def build(self, input_shape): 304 """Create variables of the Cudnn RNN. 305 306 It can be called manually before `__call__()` or automatically through 307 `__call__()`. In the former case, subsequent `__call__()`s will skip 308 creating variables. 309 Args: 310 input_shape: network input tensor shape, a python list or a TensorShape 311 object with 3 dimensions. 312 Raises: 313 ValueError: if input_shape has wrong dimension or unknown 3rd dimension. 314 """ 315 if self.built: 316 return 317 318 input_shape = tensor_shape.TensorShape(input_shape) 319 if input_shape.ndims != 3: 320 raise ValueError("Expecting input_shape with 3 dims, got %d" % 321 input_shape.ndims) 322 if input_shape[-1].value is None: 323 raise ValueError("The last dimension of the inputs to `CudnnRNN` " 324 "should be defined. Found `None`.") 325 self._input_size = input_shape[-1].value 326 self.input_spec = input_spec.InputSpec(ndim=3, axes={-1: self._input_size}) 327 328 self._set_scope(None) 329 330 # Not using base class `add_variable()` since the it calls 331 # `tf.get_variable()` with a callable initializer whereas here with a 332 # tensor. The difference is mandated to support forward-compatibility with 333 # Cudnn. 334 with vs.variable_scope( 335 self._scope, 336 reuse=self.built, 337 custom_getter=self._update_trainable_weights): 338 if self._kernel_initializer is None: 339 self._kernel_initializer = init_ops.glorot_uniform_initializer( 340 seed=self._seed, dtype=self._plain_dtype) 341 if self._bias_initializer is None: 342 self._bias_initializer = init_ops.constant_initializer( 343 0.0, dtype=self._plain_dtype) 344 345 weights = [ 346 self._kernel_initializer(sp, dtype=self._plain_dtype) 347 for sp in self.canonical_weight_shapes 348 ] 349 biases = [ 350 self._bias_initializer(sp, dtype=self._plain_dtype) 351 for sp in self.canonical_bias_shapes 352 ] 353 opaque_params_t = self._canonical_to_opaque(weights, biases) 354 355 if vs.get_variable_scope().partitioner is not None: 356 logging.warn( 357 "Partitioner is not supported for Cudnn RNN layer variables, using " 358 "it will create forward-compatibility issues with future " 359 "CUDA/CuDNN generations.") 360 # Initialize opaque params with a tensor with unknown shape, thus couldn't 361 # use self.add_variable(name, shape, initializer, ...) 362 self.kernel = vs.get_variable( 363 "opaque_kernel", dtype=self._plain_dtype, 364 initializer=opaque_params_t, validate_shape=False) 365 # Create saveable in the outer scope of the cudnn subgraph, such that 366 # alternative subgraph with platform-independent rnn cells can load the 367 # checkpoints directly. 368 if not (self.built or vs.get_variable_scope().reuse is True): 369 self._create_saveable() 370 self.built = True 371 372 def _gather_saveables_for_checkpoint(self): 373 raise NotImplementedError( 374 "This cell does not yet support object-based saving. File a feature " 375 "request if this limitation bothers you.") 376 377 def call(self, 378 inputs, 379 initial_state=None, 380 sequence_lengths=None, 381 time_major=True, 382 training=True): 383 """Runs the forward step for the RNN model. 384 385 Args: 386 inputs: `3-D` tensor. If `time_major` is True (default), the Tensor shape 387 is [time_len, batch_size, input_size]. If `time_major` is False, the 388 shape is [batch_size, time_len, input_size]. 389 initial_state: a tuple of tensor(s) of shape 390 `[num_layers * num_dirs, batch_size, num_units]` if 391 `time_major` is True (default) or `[batch_size, num_layers * num_dirs, 392 num_units]` if `time_major` is False. If not provided, use 393 zero initial states. The tuple size is 2 for LSTM and 1 for other RNNs. 394 sequence_lengths: an int32 array representing the variable sequence 395 lengths in a batch. The size of the array has to equal the 396 batch_size. If not provided, the same sequence length will be assumed. 397 time_major: The shape format of the `inputs` and `outputs` Tensors. If 398 true, these Tensors must be shaped ['max_time', 'batch_size', 'depth']. 399 If false, these Tensors must be shaped ['batch_size', 'max_time', 400 'depth']. By default this function accepts input and emits output in 401 time-major form. This param is only effective when 'sequence_lengths' 402 is used. 403 training: whether this operation will be used in training or inference. 404 Returns: 405 output: a tensor of shape `[time_len, batch_size, num_dirs * num_units]` 406 if `time_major` is True (default) or `[batch_size, time_len, 407 num_dirs * num_units]` if `time_major` is False. 408 It is a `concat([fwd_output, bak_output], axis=2)`. 409 output_states: a tuple of tensor(s) of the same shape and structure as 410 `initial_state`. 411 Raises: 412 TypeError: initial_state is not a tuple. 413 """ 414 if initial_state is not None and not isinstance(initial_state, tuple): 415 raise TypeError("Invalid initial_state type: %s, expecting tuple." % 416 initial_state) 417 dtype = self.dtype 418 inputs = ops.convert_to_tensor(inputs, dtype=dtype) 419 420 batch_size = array_ops.shape(inputs)[1] 421 if initial_state is None: 422 initial_state = self._zero_state(batch_size) 423 if self._rnn_mode == CUDNN_LSTM: 424 h, c = initial_state # pylint:disable=unbalanced-tuple-unpacking,unpacking-non-sequence 425 else: 426 h, = initial_state # pylint:disable=unbalanced-tuple-unpacking,unpacking-non-sequence 427 h = ops.convert_to_tensor(h, dtype=dtype) 428 if self._rnn_mode == CUDNN_LSTM: 429 c = ops.convert_to_tensor(c, dtype=dtype) 430 else: 431 # For model that doesn't take input_c, replace with a dummy tensor. 432 c = array_ops.constant([], dtype=dtype) 433 outputs, (output_h, output_c) = self._forward( 434 inputs, h, c, self.kernel, sequence_lengths, time_major, training) 435 if self._rnn_mode == CUDNN_LSTM: 436 return outputs, (output_h, output_c) 437 else: 438 return outputs, (output_h,) 439 440 def state_shape(self, batch_size): 441 raise NotImplementedError 442 443 def _zero_state(self, batch_size): 444 res = [] 445 for sp in self.state_shape(batch_size): 446 res.append(array_ops.zeros(sp, dtype=self.dtype)) 447 return tuple(res) 448 449 def _canonical_weight_shape(self, layer): 450 """Shapes of Cudnn canonical weight tensors for given layer.""" 451 if layer < 0 or layer >= self._num_layers: 452 raise ValueError("\'layer\' is not valid, got %s, expecting [%d, %d]" % 453 (layer, 0, self._num_layers-1)) 454 if not self._input_size: 455 raise RuntimeError( 456 "%s._canonical_weight_shape invoked before input shape is known" % 457 type(self).__name__) 458 459 input_size = self._input_size 460 num_units = self._num_units 461 num_gates = self._num_params_per_layer // 2 462 is_bidi = self._direction == CUDNN_RNN_BIDIRECTION 463 464 if layer == 0: 465 wts_applied_on_inputs = [(num_units, input_size)] * num_gates 466 else: 467 if is_bidi: 468 wts_applied_on_inputs = [(num_units, 2 * num_units)] * num_gates 469 else: 470 wts_applied_on_inputs = [(num_units, num_units)] * num_gates 471 wts_applied_on_hidden_states = [(num_units, num_units)] * num_gates 472 tf_wts = wts_applied_on_inputs + wts_applied_on_hidden_states 473 return tf_wts if not is_bidi else tf_wts * 2 474 475 def _canonical_bias_shape(self, unused_layer): 476 """Shapes of Cudnn canonical bias tensors for given layer.""" 477 num_dirs = 1 if self._direction == CUDNN_RNN_UNIDIRECTION else 2 478 return [[self._num_units]] * num_dirs * self._num_params_per_layer 479 480 def _canonical_to_opaque(self, cu_weights, cu_biases): 481 if not self._input_size: 482 raise RuntimeError( 483 "%s._canonical_to_opaque invoked before input shape is known" % 484 type(self).__name__) 485 with ops.device("/gpu:0"): 486 return cudnn_rnn_ops.cudnn_rnn_canonical_to_opaque_params( 487 rnn_mode=self._rnn_mode, 488 num_layers=self._num_layers, 489 num_units=self._num_units, 490 input_size=self._input_size, 491 weights=cu_weights, 492 biases=cu_biases, 493 input_mode=self._input_mode, 494 seed=self._seed, 495 dropout=self._dropout, 496 direction=self._direction) 497 498 def _forward(self, inputs, h, c, opaque_params, sequence_lengths, time_major, 499 training): 500 output, output_h, output_c = cudnn_rnn_ops._cudnn_rnn( # pylint:disable=protected-access 501 inputs, 502 h, 503 c, 504 opaque_params, 505 training, 506 self._rnn_mode, 507 sequence_lengths=sequence_lengths, 508 time_major=time_major, 509 input_mode=self._input_mode, 510 direction=self._direction, 511 dropout=self._dropout, 512 seed=self._seed) 513 return output, (output_h, output_c) 514 515 def _create_saveable(self): 516 """Create custom saveable for the Cudnn layer. 517 518 Called during layer building process to make sharing checkpoints between 519 Cudnn and Cudnn-compatible RNNs easy. 520 Returns: 521 a `CudnnOpaqueParamsSaveable` object. 522 Raises: 523 RuntimeError: if any custom saveable is already created for this layer. 524 """ 525 if self._saveable is not None: 526 raise RuntimeError("Cudnn saveable already created.") 527 self._saveable = self._saveable_cls( # pylint:disable=not-callable 528 opaque_params=self.trainable_variables[0], 529 num_layers=self.num_layers, 530 num_units=self.num_units, 531 input_size=self.input_size, 532 input_mode=self.input_mode, 533 direction=self.direction, 534 scope=vs.get_variable_scope(), 535 name="%s_saveable" % self.trainable_variables[0].name.split(":")[0]) 536 self._saveable._add_trackable_dependencies( # pylint: disable=protected-access 537 trackable=self, dtype=self._plain_dtype) 538 ops.add_to_collection(ops.GraphKeys.SAVEABLE_OBJECTS, self._saveable) 539 540 541class CudnnLSTM(_CudnnRNN): 542 """Cudnn implementation of LSTM layer.""" 543 _rnn_mode = CUDNN_LSTM 544 _num_params_per_layer = CUDNN_LSTM_PARAMS_PER_LAYER 545 _saveable_cls = cudnn_rnn_ops.CudnnLSTMSaveable 546 547 def state_shape(self, batch_size): 548 """Shape of Cudnn LSTM states. 549 550 Shape is a 2-element tuple. Each is 551 [num_layers * num_dirs, batch_size, num_units] 552 Args: 553 batch_size: an int 554 Returns: 555 a tuple of python arrays. 556 """ 557 return ([self.num_layers * self.num_dirs, batch_size, self.num_units], 558 [self.num_layers * self.num_dirs, batch_size, self.num_units]) 559 560 @property 561 def _gather_saveables_for_checkpoint(self): 562 if self._direction == CUDNN_RNN_UNIDIRECTION: 563 # Skip one inheritance level to avoid NotImplementedError. 564 return super(_CudnnRNN, self)._gather_saveables_for_checkpoint 565 else: 566 raise NotImplementedError( 567 "Object-based saving does not currently support bidirectional LSTM " 568 "cells. File a feature request if this limitation bothers you.") 569 570 571class _CudnnRNNNoInputC(_CudnnRNN): 572 """Abstract simple CudnnRNN layer without input_c.""" 573 574 def state_shape(self, batch_size): 575 """Shape of the state of Cudnn RNN cells w/o. input_c. 576 577 Shape is a 1-element tuple, 578 [num_layers * num_dirs, batch_size, num_units] 579 Args: 580 batch_size: an int 581 Returns: 582 a tuple of python arrays. 583 """ 584 return [self.num_layers * self.num_dirs, batch_size, self.num_units], 585 586 587class CudnnGRU(_CudnnRNNNoInputC): 588 """Cudnn implementation of the GRU layer.""" 589 _rnn_mode = CUDNN_GRU 590 _num_params_per_layer = CUDNN_GRU_PARAMS_PER_LAYER 591 _saveable_cls = cudnn_rnn_ops.CudnnGRUSaveable 592 593 594class CudnnRNNTanh(_CudnnRNNNoInputC): 595 """Cudnn implementation of the RNN-tanh layer.""" 596 _rnn_mode = CUDNN_RNN_TANH 597 _num_params_per_layer = CUDNN_RNN_TANH_PARAMS_PER_LAYER 598 _saveable_cls = cudnn_rnn_ops.CudnnRNNTanhSaveable 599 600 601class CudnnRNNRelu(_CudnnRNNNoInputC): 602 """Cudnn implementation of the RNN-relu layer.""" 603 _rnn_mode = CUDNN_RNN_RELU 604 _num_params_per_layer = CUDNN_RNN_RELU_PARAMS_PER_LAYER 605 _saveable_cls = cudnn_rnn_ops.CudnnRNNReluSaveable 606