docs: protected void calcBackpropGradients(boolean clearLayers, boolean truncatedBPTT, INDArray... externalEpsilons) {
          if (flattenedGradients == null) {
              initGradientsView();
          }
  
          /*
           Design for workspaces use in backprop for ComputationGraph is similar to MultiLayerNetwork and shares some
           features with outputOfLayersDetached
  
           Specifically:
           1. We assume forward pass has already been done, and hence layer input fields are set (with all arrays/activations in
              workspace WS_ALL_LAYERS_ACT if appropriate)
           2. We use a set of small workspaces to contain the activation gradients for a single layer
              These are opened once per layer, and are closed only once the corresponding activation gradients have been
              consumed by all layers
           */
  
          if(externalEpsilons == null || externalEpsilons.length == 0 && configuration.getTrainingWorkspaceMode() != WorkspaceMode.NONE){
              WorkspaceUtils.assertOpenAndActive(WS_ALL_LAYERS_ACT, "Expected workspace WS_ALL_LAYERS_ACT to be active and open" +
                      " in calcBackpropGradients when workspace mode is not set to NONE");
          }
  
          //Validate the network configuration for external errors - no output layers
          if(externalEpsilons != null && externalEpsilons.length > 0){
              List<String> outputLayers = configuration.getNetworkOutputs();
              for(String s : outputLayers ){
                  GraphVertex gv = getVertex(s);
                  if(gv instanceof LayerVertex && ((LayerVertex)gv).getLayer() instanceof IOutputLayer){
                      throw new IllegalStateException("Cannot perform backprop with external errors in conjunction with an output layer:" +
                              " output layers cannot use external errors for backprop. Layer name: " + s);
                  }
              }
  
          }
  
          //Position in array: index of vertex. Value at position: the step (in topological order) that the activation
          // gradients of the specified vertex have been consumed by
          //Put another way: this is the step that it's safe to deallocate the layer's activation gradients by closing the
          // corresponding workspace
          //TODO we can probably cache this...
          int[] vertexActGradsFullyConsumedByStep = new int[topologicalOrder.length];
          for(GraphVertex gv : vertices){
              int idx = gv.getVertexIndex();
              int minStepOfInputFrom = Integer.MAX_VALUE;
              VertexIndices[] inputsFrom = gv.getInputVertices();
              if(inputsFrom != null) {
                  //inputsFrom may be null for input vertex
                  for (VertexIndices vi : inputsFrom) {
                      int posInTopoSort = ArrayUtils.indexOf(topologicalOrder, vi.getVertexIndex());
                      if (posInTopoSort == -1) {
                          throw new IllegalStateException("Did not find vertex " + vi.getVertexIndex() + " in topological sort array");
                      }
                      minStepOfInputFrom = Math.min(minStepOfInputFrom, posInTopoSort);
                  }
              }
  
              if(minStepOfInputFrom == Integer.MAX_VALUE){
                  //Input vertex, etc
                  vertexActGradsFullyConsumedByStep[idx] = 0;
              } else {
                  vertexActGradsFullyConsumedByStep[idx] = minStepOfInputFrom;
              }
          }
  
  
          boolean noWS = configuration.getInferenceWorkspaceMode() == WorkspaceMode.NONE;
          LayerWorkspaceMgr allNone = noWS ? LayerWorkspaceMgr.noWorkspaces(helperWorkspaces) : null;
  
          List<LayerWorkspaceMgr> allWorkspaceManagers = new ArrayList<>();
          List<LayerWorkspaceMgr> freeWorkspaceManagers = new ArrayList<>();  //Basically used as a stack
          Map<MemoryWorkspace, LayerWorkspaceMgr> openActivationsWorkspaces = new IdentityHashMap<>();
          List<MemoryWorkspace>[] closeAtEndIteraton = (List<MemoryWorkspace>[])new List[topologicalOrder.length];
  
          //Do backprop, in reverse topological order
          LinkedList<Triple<String, INDArray, Character>> gradients = new LinkedList<>();
          boolean[] setVertexEpsilon = new boolean[topologicalOrder.length]; //If true: already set epsilon for this vertex; later epsilons should be *added* to the existing one, not set
          MemoryWorkspace initialWorkspace = Nd4j.getMemoryManager().getCurrentWorkspace();
          try{
              for(int i=topologicalOrder.length-1; i>= 0; i--){
                  boolean hitFrozen = false;
                  GraphVertex current = vertices[topologicalOrder[i]];
                  int vIdx = current.getVertexIndex();
                  String vertexName = current.getVertexName();
  
                  //FIXME: make the frozen vertex feature extraction more flexible
                  if (current.hasLayer() && current.getLayer() instanceof FrozenLayer || current instanceof FrozenVertex){
                      hitFrozen = true;
                  }
  
                  if (current.isInputVertex() || hitFrozen){
                      //Close any activation gradient workspaces that we no longer require
                      //Note that activation gradient workspaces can be closed only once the corresponding activations
                      // gradients have been fully consumed
                      if(closeAtEndIteraton[i] != null){
                          for(MemoryWorkspace wsAct : closeAtEndIteraton[i]){
                              wsAct.close();
                              LayerWorkspaceMgr canNowReuse = openActivationsWorkspaces.remove(wsAct);
                              freeWorkspaceManagers.add(canNowReuse);
                          }
                      }
                      closeAtEndIteraton[i] = null;
                      continue;
                  }
  
  
                  //First: determine what workspace manager we should use for the activation gradients from this vertex
                  LayerWorkspaceMgr workspaceMgr;
                  if (noWS) {
                      workspaceMgr = allNone;
                  } else {
                      //First: is there a free activation gradient workspace we can use?
                      if (freeWorkspaceManagers.size() > 0) {
                          workspaceMgr = freeWorkspaceManagers.remove(freeWorkspaceManagers.size() - 1);
                      } else {
                          //No existing free workspace managers for forward pass - create a new one...
                          String wsName = "WS_LAYER_ACT_" + allWorkspaceManagers.size();
                          workspaceMgr = LayerWorkspaceMgr.builder()
                                  .with(ArrayType.INPUT, WS_ALL_LAYERS_ACT, WS_ALL_LAYERS_ACT_CONFIG)
                                  .with(ArrayType.ACTIVATION_GRAD, wsName, WS_LAYER_ACT_X_CONFIG)
                                  .with(ArrayType.ACTIVATIONS, WS_LAYER_WORKING_MEM, WS_LAYER_WORKING_MEM_CONFIG) //For forward pass in the context of BP
                                  .with(ArrayType.FF_WORKING_MEM, WS_LAYER_WORKING_MEM, WS_LAYER_WORKING_MEM_CONFIG)
                                  .with(ArrayType.BP_WORKING_MEM, WS_LAYER_WORKING_MEM, WS_LAYER_WORKING_MEM_CONFIG)
                                  .with(ArrayType.RNN_FF_LOOP_WORKING_MEM, WS_RNN_LOOP_WORKING_MEM, WS_RNN_LOOP_WORKING_MEM_CONFIG)
                                  .with(ArrayType.RNN_BP_LOOP_WORKING_MEM, WS_RNN_LOOP_WORKING_MEM, WS_RNN_LOOP_WORKING_MEM_CONFIG)
                                  .build();
  
                          allWorkspaceManagers.add(workspaceMgr);
                      }
                  }
                  workspaceMgr.setHelperWorkspacePointers(helperWorkspaces);
  
                  if (current.isOutputVertex()) {
                      //Two reasons for a vertex to be an output vertex:
                      //(a) it's an output layer (i.e., instanceof IOutputLayer), or
                      //(b) it's a normal layer, but it has been marked as an output layer for use in external errors - for reinforcement learning, for example
  
                      int thisOutputNumber = configuration.getNetworkOutputs().indexOf(current.getVertexName());
                      Layer currentLayer = current.getLayer();
                      if (currentLayer instanceof FrozenLayerWithBackprop) {
                          currentLayer = ((FrozenLayerWithBackprop) currentLayer).getInsideLayer();
                      }
                      if (currentLayer instanceof IOutputLayer) {
                          IOutputLayer outputLayer = (IOutputLayer) currentLayer;
  
                          INDArray currLabels = labels[thisOutputNumber];
                          outputLayer.setLabels(currLabels);
                      } else {
                          if ((externalEpsilons == null || externalEpsilons.length == 0)
                                  && labels[thisOutputNumber] != null) {
                              throw new DL4JException("Layer \"" + current.getVertexName() + "\" of type "
                                      + current.getLayer().getClass().getSimpleName()
                                      + " is set as network output "
                                      + "(but isn't an IOutputLayer). Only IOutputLayer layers can be fit via backprop with"
                                      + " a labels array. ");
                          }
                          current.setEpsilon(externalEpsilons[thisOutputNumber]);
                          setVertexEpsilon[topologicalOrder[i]] = true;
                      }
                  }
  
                  //Actually execute backprop for the specified vertex
                  //First: Open the relevant workspace for the activations.
                  //Note that this will be closed only once the current vertex's activations have been consumed
                  MemoryWorkspace wsActivationGrads = workspaceMgr.notifyScopeEntered(ArrayType.ACTIVATION_GRAD);
                  openActivationsWorkspaces.put(wsActivationGrads, workspaceMgr);
  
                  //Note that because we're opening activation gradient workspaces not in any defined order (i.e., workspace
                  // use isn't simply nested), we'll manually override the previous workspace setting. Otherwise, when we
                  // close these workspaces, the "current" workspace may be set to the incorrect one
                  wsActivationGrads.setPreviousWorkspace(initialWorkspace);
  
                  int closeableAt = vertexActGradsFullyConsumedByStep[vIdx];
                  if(closeableAt >= 0) {
                      if (closeAtEndIteraton[closeableAt] == null) {
                          closeAtEndIteraton[closeableAt] = new ArrayList<>();
                      }
                      closeAtEndIteraton[closeableAt].add(wsActivationGrads);
                  }
  
                  Pair<Gradient, INDArray[]> pair;
                  INDArray[] epsilons;
                  try(MemoryWorkspace wsWorkingMem = workspaceMgr.notifyScopeEntered(ArrayType.BP_WORKING_MEM)){
                      pair = current.doBackward(truncatedBPTT, workspaceMgr);
                      epsilons = pair.getSecond();
  
                      //Validate workspace location for the activation gradients:
                      //validateArrayWorkspaces(LayerWorkspaceMgr mgr, INDArray array, ArrayType arrayType, String vertexName, boolean isInputVertex, String op){
                      for (INDArray epsilon : epsilons) {
                          if(epsilon != null) {
                              //May be null for EmbeddingLayer, etc
                              validateArrayWorkspaces(workspaceMgr, epsilon, ArrayType.ACTIVATION_GRAD, vertexName, false, "Backprop");
                          }
                      }
                  }
  
                  //Inputs to the current GraphVertex:
                  VertexIndices[] inputVertices = current.getInputVertices();
  
                  //Set epsilons for the vertices that provide inputs to this vertex:
                  if (inputVertices != null) {
                      int j = 0;
                      for (VertexIndices v : inputVertices) {
                          GraphVertex gv = vertices[v.getVertexIndex()];
                          if (setVertexEpsilon[gv.getVertexIndex()]) {
                              //This vertex: must output to multiple vertices... we want to add the epsilons here
                              INDArray currentEps = gv.getEpsilon();
                              gv.setEpsilon(currentEps.addi(epsilons[j++]));  //TODO is this always safe?
                          } else {
                              gv.setEpsilon(epsilons[j++]);
                          }
                          setVertexEpsilon[gv.getVertexIndex()] = true;
                      }
                  }
  
                  if (pair.getFirst() != null) {
                      Gradient g = pair.getFirst();
                      Map<String, INDArray> map = g.gradientForVariable();
                      LinkedList<Triple<String, INDArray, Character>> tempList = new LinkedList<>();
                      for (Map.Entry<String, INDArray> entry : map.entrySet()) {
                          String origName = entry.getKey();
                          String newName = current.getVertexName() + "_" + origName;
                          tempList.addFirst(new Triple<>(newName, entry.getValue(),
                                  g.flatteningOrderForVariable(origName)));
                      }
                      for (Triple<String, INDArray, Character> t : tempList)
                          gradients.addFirst(t);
                  }
  
                  //Close any activation gradient workspaces that we no longer require
                  //Note that activation gradient workspaces can be closed only once the corresponding activations
                  // gradients have been fully consumed
                  if(closeAtEndIteraton[i] != null){
                      for(MemoryWorkspace wsAct : closeAtEndIteraton[i]){
                          wsAct.close();
                          LayerWorkspaceMgr canNowReuse = openActivationsWorkspaces.remove(wsAct);
                          freeWorkspaceManagers.add(canNowReuse);
                      }
                      closeAtEndIteraton[i] = null;
                  }
              }
  
          } finally {
              //Close all open workspaces... usually this list will be empty, but not if an exception is thrown
              for(MemoryWorkspace ws : openActivationsWorkspaces.keySet()){
                  ws.close();
              }
              Nd4j.getMemoryManager().setCurrentWorkspace(initialWorkspace);
          }
  
          //Now, add the gradients in the order we need them in for flattening (same as params order)
          Gradient gradient = new DefaultGradient(flattenedGradients);
          for (Triple<String, INDArray, Character> t : gradients) {
              gradient.setGradientFor(t.getFirst(), t.getSecond(), t.getThird());
          }
  
          this.gradient = gradient;
  
          if(truncatedBPTT && clearTbpttState){
              rnnClearPreviousState();
          }
  
          //Clear inputs and epsilons:
          if(clearLayers) {
              for (GraphVertex gv : vertices) {
                  gv.clear();
              }
          }
      }