docs: static public FwdPassReturn activateHelper(final BaseLayer layer, final NeuralNetConfiguration conf,
                                                 final IActivation gateActivationFn, //Activation function for the gates - sigmoid or hard sigmoid (must be found in range 0 to 1)
                                                 INDArray input, final INDArray recurrentWeights, //Shape: [hiddenLayerSize,4*hiddenLayerSize+3]; order: [wI,wF,wO,wG,wFF,wOO,wGG]
                                                 final INDArray originalInputWeights, //Shape: [n^(L-1),4*hiddenLayerSize]; order: [wi,wf,wo,wg]
                                                 final INDArray biases, //Shape: [4,hiddenLayerSize]; order: [bi,bf,bo,bg]^T
                                                 final boolean training, final INDArray originalPrevOutputActivations,
                                                 final INDArray originalPrevMemCellState, boolean forBackprop, boolean forwards,
                                                 final String inputWeightKey, INDArray maskArray, //Input mask: should only be used with bidirectional RNNs + variable length
                                                 final boolean hasPeepholeConnections, //True for GravesLSTM, false for LSTM
                                                 final LSTMHelper helper, final CacheMode cacheMode, // cacheMode for layer calling this helper
                                                 final LayerWorkspaceMgr workspaceMgr
                                                 ) {
  
          //Mini-batch data format: for mini-batch size m, nIn inputs, and T time series length
          //Data has shape [m,nIn,T]. Layer activations/output has shape [m,nHiddenUnits,T]
          if (input == null || input.length() == 0)
              throw new IllegalArgumentException("Invalid input: not set or 0 length");
  
          INDArray inputWeights = originalInputWeights;
          INDArray prevOutputActivations = originalPrevOutputActivations;
  
          if(maskArray != null){
              maskArray = maskArray.castTo(recurrentWeights.dataType());
          }
  
          boolean is2dInput = input.rank() < 3; //Edge case of T=1, may have shape [m,nIn], equiv. to [m,nIn,1]
  
          input = input.castTo(inputWeights.dataType());  //No-op if already correct dtype
  
          // FIXME
          int timeSeriesLength = (int) (is2dInput ? 1 : input.size(2));
          int hiddenLayerSize = (int) recurrentWeights.size(0);
          int miniBatchSize = (int) input.size(0);
  
          INDArray prevMemCellState;
          if (originalPrevMemCellState == null) {
              prevMemCellState = Nd4j.create(inputWeights.dataType(), new long[] {miniBatchSize, hiddenLayerSize}, 'f');
          } else {
              prevMemCellState = originalPrevMemCellState.dup('f');
          }
  
  
          INDArray recurrentWeightsIFOG = recurrentWeights.get(all(), interval(0, 4 * hiddenLayerSize)).dup('f');
  
          INDArray wFFTranspose = null;
          INDArray wOOTranspose = null;
          INDArray wGGTranspose = null;
  
          if (hasPeepholeConnections) {
              wFFTranspose = recurrentWeights.get(all(), interval(4 * hiddenLayerSize, 4 * hiddenLayerSize + 1)).reshape(1, recurrentWeights.size(0));//current
              wOOTranspose = recurrentWeights.get(all(), interval(4 * hiddenLayerSize + 1, 4 * hiddenLayerSize + 2)).reshape(1, recurrentWeights.size(0)); //current
              wGGTranspose = recurrentWeights.get(all(), interval(4 * hiddenLayerSize + 2, 4 * hiddenLayerSize + 3)).reshape(1, recurrentWeights.size(0)); //previous
  
              if (timeSeriesLength > 1 || forBackprop) {
                  wFFTranspose = Shape.toMmulCompatible(wFFTranspose);
                  wOOTranspose = Shape.toMmulCompatible(wOOTranspose);
                  wGGTranspose = Shape.toMmulCompatible(wGGTranspose);
              }
          }
  
          //Allocate arrays for activations:
          boolean sigmoidGates = gateActivationFn instanceof ActivationSigmoid;
          IActivation afn = layer.layerConf().getActivationFn();
          INDArray outputActivations = null;
  
          FwdPassReturn toReturn = new FwdPassReturn();
          if (forBackprop) {
              toReturn.fwdPassOutputAsArrays = new INDArray[timeSeriesLength];
              toReturn.memCellState = new INDArray[timeSeriesLength];
              toReturn.memCellActivations = new INDArray[timeSeriesLength];
              toReturn.iz = new INDArray[timeSeriesLength];
              toReturn.ia = new INDArray[timeSeriesLength];
              toReturn.fa = new INDArray[timeSeriesLength];
              toReturn.oa = new INDArray[timeSeriesLength];
              toReturn.ga = new INDArray[timeSeriesLength];
              if (!sigmoidGates) {
                  toReturn.fz = new INDArray[timeSeriesLength];
                  toReturn.oz = new INDArray[timeSeriesLength];
                  toReturn.gz = new INDArray[timeSeriesLength];
              }
  
              if (training && cacheMode != CacheMode.NONE && workspaceMgr.hasConfiguration(ArrayType.FF_CACHE) && workspaceMgr.isWorkspaceOpen(ArrayType.FF_CACHE)) {
                  try (MemoryWorkspace wsB = workspaceMgr.notifyScopeBorrowed(ArrayType.FF_CACHE)) {
                      outputActivations = Nd4j.create(inputWeights.dataType(), new long[] {miniBatchSize, hiddenLayerSize, timeSeriesLength}, 'f'); //F order to keep time steps together
                      toReturn.fwdPassOutput = outputActivations;
                  }
              } else {
                  outputActivations = workspaceMgr.create(ArrayType.ACTIVATIONS, input.dataType(), new long[] {miniBatchSize, hiddenLayerSize, timeSeriesLength}, 'f'); //F order to keep time steps together
                  toReturn.fwdPassOutput = outputActivations;
              }
          } else {
              outputActivations = workspaceMgr.create(ArrayType.ACTIVATIONS, input.dataType(), new long[] {miniBatchSize, hiddenLayerSize, timeSeriesLength}, 'f'); //F order to keep time steps together
              toReturn.fwdPassOutput = outputActivations;
          }
  
          //Level1 l1BLAS = Nd4j.getBlasWrapper().level1();
  
          //Input validation: check input data matches nIn
          if (input.size(1) != inputWeights.size(0)) {
              throw new DL4JInvalidInputException("Received input with size(1) = " + input.size(1)
                              + " (input array shape = " + Arrays.toString(input.shape())
                              + "); input.size(1) must match layer nIn size (nIn = " + inputWeights.size(0) + ")");
          }
          //Input validation: check that if past state is provided, that it has same
          //These can be different if user forgets to call rnnClearPreviousState() between calls of rnnTimeStep
          if (prevOutputActivations != null && prevOutputActivations.size(0) != input.size(0)) {
              throw new DL4JInvalidInputException("Previous activations (stored state) number of examples = "
                              + prevOutputActivations.size(0) + " but input array number of examples = " + input.size(0)
                              + ". Possible cause: using rnnTimeStep() without calling"
                              + " rnnClearPreviousState() between different sequences?");
          }
  
          //initialize prevOutputActivations to zeroes
          if (prevOutputActivations == null) {
              prevOutputActivations = Nd4j.zeros(input.dataType(), new long[] {miniBatchSize, hiddenLayerSize});
          }
  
          if (helper != null) {
              FwdPassReturn ret = helper.activate(layer, conf, gateActivationFn, input, recurrentWeights, inputWeights,
                              biases, training, prevOutputActivations, prevMemCellState, forBackprop, forwards,
                              inputWeightKey, maskArray, hasPeepholeConnections, workspaceMgr);
              if (ret != null) {
                  return ret;
              }
          }
  
          for (int iTimeIndex = 0; iTimeIndex < timeSeriesLength; iTimeIndex++) {
              try(MemoryWorkspace ws = workspaceMgr.notifyScopeEntered(ArrayType.RNN_FF_LOOP_WORKING_MEM)) {
                  int time = iTimeIndex;
  
                  if (!forwards) {
                      time = timeSeriesLength - iTimeIndex - 1;
                  }
  
  
                  INDArray miniBatchData = (is2dInput ? input : input.tensorAlongDimension(time, 1, 0)); //[Expected shape: [m,nIn]. Also deals with edge case of T=1, with 'time series' data of shape [m,nIn], equiv. to [m,nIn,1]
                  miniBatchData = Shape.toMmulCompatible(miniBatchData);
  
                  // if we're using cache here - let's create ifogActivations within cache workspace, so all views from this array will be valid in cache
                  cacheEnter(training, cacheMode, workspaceMgr);
  
                  //Calculate activations for: network input + forget, output, input modulation gates. Next 3 lines are first part of those
                  INDArray ifogActivations = miniBatchData.mmul(inputWeights); //Shape: [miniBatch,4*layerSize]
                  cacheExit(training, cacheMode, workspaceMgr);
  
                  Nd4j.gemm(prevOutputActivations, recurrentWeightsIFOG, ifogActivations, false, false, 1.0, 1.0);
                  ifogActivations.addiRowVector(biases);
  
                  INDArray inputActivations =
                          ifogActivations.get(all(), interval(0, hiddenLayerSize));
                  if (forBackprop) {
                      if(shouldCache(training, cacheMode, workspaceMgr)){
                          cacheEnter(training, cacheMode, workspaceMgr);
                          toReturn.iz[time] = inputActivations.dup('f');
                          cacheExit(training, cacheMode, workspaceMgr);
                      } else {
                          toReturn.iz[time] = workspaceMgr.dup(ArrayType.BP_WORKING_MEM, inputActivations, 'f');
                      }
                  }
                  layer.layerConf().getActivationFn().getActivation(inputActivations, training);
                  if (forBackprop){
                      if(shouldCache(training, cacheMode, workspaceMgr)){
                          cacheEnter(training, cacheMode, workspaceMgr);
                          toReturn.ia[time] = inputActivations.dup('f');
                          cacheExit(training, cacheMode, workspaceMgr);
                      } else {
                          toReturn.ia[time] = workspaceMgr.leverageTo(ArrayType.BP_WORKING_MEM, inputActivations);
                      }
                  }
  
                  INDArray forgetGateActivations = ifogActivations.get(all(),
                          interval(hiddenLayerSize, 2 * hiddenLayerSize));
                  if (hasPeepholeConnections) {
                      INDArray pmcellWFF = prevMemCellState.dup('f').muliRowVector(wFFTranspose);
                      forgetGateActivations.addi(pmcellWFF);
                  }
                  //Above line: treats matrix as a vector. Can only do this because we're sure both pwcelWFF and forgetGateACtivations are f order, offset 0 and have same strides
                  if (forBackprop && !sigmoidGates) {
                      if(shouldCache(training, cacheMode, workspaceMgr)){
                          cacheEnter(training, cacheMode, workspaceMgr);
                          toReturn.fz[time] = forgetGateActivations.dup('f'); //Forget gate pre-out (z)
                          cacheExit(training, cacheMode, workspaceMgr);
                      } else {
                          toReturn.fz[time] = workspaceMgr.dup(ArrayType.BP_WORKING_MEM, forgetGateActivations, 'f'); //Forget gate pre-out (z)
                      }
                  }
                  gateActivationFn.getActivation(forgetGateActivations, training);
  
                  if (forBackprop) {
                      if(shouldCache(training, cacheMode, workspaceMgr)){
                          cacheEnter(training, cacheMode, workspaceMgr);
                          toReturn.fa[time] = forgetGateActivations.dup('f');
                          cacheExit(training, cacheMode, workspaceMgr);
                      } else {
                          toReturn.fa[time] = workspaceMgr.leverageTo(ArrayType.BP_WORKING_MEM, forgetGateActivations);
                      }
                  }
  
  
                  INDArray inputModGateActivations = ifogActivations.get(all(),
                          interval(3 * hiddenLayerSize, 4 * hiddenLayerSize));
                  if (hasPeepholeConnections) {
                      INDArray pmcellWGG = prevMemCellState.dup('f').muliRowVector(wGGTranspose);
                      inputModGateActivations.addi(pmcellWGG);
                  }
                  if (forBackprop && !sigmoidGates) {
                      cacheEnter(training, cacheMode, workspaceMgr);
                      toReturn.gz[time] = workspaceMgr.dup(ArrayType.BP_WORKING_MEM, inputModGateActivations, 'f'); //Input modulation gate pre-out (z)
                      cacheExit(training, cacheMode, workspaceMgr);
                  }
                  gateActivationFn.getActivation(inputModGateActivations, training);
                  if (forBackprop){
                      if(shouldCache(training, cacheMode, workspaceMgr)){
                          cacheEnter(training, cacheMode, workspaceMgr);
                          toReturn.ga[time] = inputModGateActivations.dup('f');
                          cacheExit(training, cacheMode, workspaceMgr);
                      } else {
                          toReturn.ga[time] = workspaceMgr.leverageTo(ArrayType.BP_WORKING_MEM, inputModGateActivations);
                      }
                  }
  
                  //Memory cell state
                  INDArray currentMemoryCellState;
                  INDArray inputModMulInput;
                  if (forBackprop) {
                      cacheEnter(training, cacheMode, workspaceMgr);
                      currentMemoryCellState = workspaceMgr.dup(ArrayType.BP_WORKING_MEM, prevMemCellState, 'f').muli(forgetGateActivations);
                      cacheExit(training, cacheMode, workspaceMgr);
                      // this variable isn't stored in cache
                      inputModMulInput = inputModGateActivations.dup('f').muli(inputActivations);
                  } else {
                      currentMemoryCellState = workspaceMgr.leverageTo(ArrayType.FF_WORKING_MEM, forgetGateActivations.muli(prevMemCellState));       //TODO optimize without the copy
                      inputModMulInput = inputModGateActivations.muli(inputActivations);
                  }
                  currentMemoryCellState.addi(inputModMulInput);
  
                  INDArray outputGateActivations = ifogActivations.get(all(),
                          interval(2 * hiddenLayerSize, 3 * hiddenLayerSize));
                  if (hasPeepholeConnections) {
                      INDArray pmcellWOO = currentMemoryCellState.dup('f').muliRowVector(wOOTranspose);
                      outputGateActivations.addi(pmcellWOO);
                  }
                  if (forBackprop && !sigmoidGates) {
                      cacheEnter(training, cacheMode, workspaceMgr);
                      toReturn.oz[time] = workspaceMgr.dup(ArrayType.BP_WORKING_MEM, outputGateActivations, 'f'); //Output gate activations
                      cacheExit(training, cacheMode, workspaceMgr);
                  }
                  gateActivationFn.getActivation(outputGateActivations, training);
                  if (forBackprop) {
                      if(shouldCache(training, cacheMode, workspaceMgr)){
                          cacheEnter(training, cacheMode, workspaceMgr);
                          toReturn.oa[time] = outputGateActivations.dup('f');
                          cacheExit(training, cacheMode, workspaceMgr);
                      } else {
                          toReturn.oa[time] = workspaceMgr.leverageTo(ArrayType.BP_WORKING_MEM, outputGateActivations);   //TODO optimize without leverage
                      }
                  }
  
  
                  ////////////// same as with iFogActivations - if we use cache, let's create this array right there
                  cacheEnter(training, cacheMode, workspaceMgr);
                  //LSTM unit outputs:
                  INDArray currMemoryCellActivation ;
                  currMemoryCellActivation = workspaceMgr.dup(ArrayType.FF_WORKING_MEM, currentMemoryCellState, 'f');
                  currMemoryCellActivation = afn.getActivation(currMemoryCellActivation, training);
                  cacheExit(training, cacheMode, workspaceMgr);
                  ///////////////////
  
                  INDArray currHiddenUnitActivations;
                  if (forBackprop) {
                      cacheEnter(training, cacheMode, workspaceMgr);
                      currHiddenUnitActivations = workspaceMgr.dup(ArrayType.BP_WORKING_MEM, currMemoryCellActivation, 'f').muli(outputGateActivations); //Expected shape: [m,hiddenLayerSize]
                      cacheExit(training, cacheMode, workspaceMgr);
                  } else {
                      currHiddenUnitActivations = currMemoryCellActivation.muli(outputGateActivations); //Expected shape: [m,hiddenLayerSize]
                  }
  
                  if (maskArray != null) {
                      //Mask array is present: bidirectional RNN -> need to zero out these activations to avoid
                      // incorrectly using activations from masked time steps (i.e., want 0 initialization in both directions)
                      //We *also* need to apply this to the memory cells, as they are carried forward
                      //Mask array has shape [minibatch, timeSeriesLength] -> get column
                      INDArray timeStepMaskColumn = maskArray.getColumn(time, true);
                      currHiddenUnitActivations.muliColumnVector(timeStepMaskColumn);
                      currentMemoryCellState.muliColumnVector(timeStepMaskColumn);
                  }
  
                  currentMemoryCellState = workspaceMgr.leverageTo(ArrayType.FF_WORKING_MEM, currentMemoryCellState); //TODO optimize, without the leverage
                  if (forBackprop) {
                      toReturn.fwdPassOutputAsArrays[time] = currHiddenUnitActivations;
                      toReturn.memCellState[time] = currentMemoryCellState;
                      toReturn.memCellActivations[time] = currMemoryCellActivation;
  
                      if (training && cacheMode != CacheMode.NONE && workspaceMgr.hasConfiguration(ArrayType.FF_CACHE) && workspaceMgr.isWorkspaceOpen(ArrayType.FF_CACHE)) {
                          toReturn.memCellActivations[time] = workspaceMgr.leverageTo(ArrayType.FF_CACHE, toReturn.memCellActivations[time]);
                          toReturn.memCellState[time] = workspaceMgr.leverageTo(ArrayType.FF_CACHE, toReturn.memCellState[time]);
                      }
  
                      if (cacheMode != CacheMode.NONE) {
                          outputActivations.tensorAlongDimension(time, 1, 0).assign(currHiddenUnitActivations);
                      }
                  } else {
                      outputActivations.tensorAlongDimension(time, 1, 0).assign(currHiddenUnitActivations);
                  }
  
                  prevOutputActivations = currHiddenUnitActivations;
                  prevMemCellState = currentMemoryCellState;
  
                  // no need to dup here, if that's cache - it's already within Cache workspace
                  toReturn.lastAct = currHiddenUnitActivations;
  
                  // the same as above, already in cache
                  toReturn.lastMemCell = currentMemoryCellState;
              }
          }
  
  
  
          //toReturn.leverageTo(ComputationGraph.workspaceExternal);
  
          toReturn.prevAct = originalPrevOutputActivations;
          toReturn.prevMemCell = originalPrevMemCellState;
  
          return toReturn;
      }