docs: protected INDArray[] outputOfLayersDetached(boolean train, @NonNull FwdPassType fwdPassType, @NonNull int[] layerIndexes, @NonNull INDArray[] features,
                                                  INDArray[] fMask, INDArray[] lMasks, boolean clearLayerInputs, boolean detachedInputs, MemoryWorkspace outputWorkspace){
          if(features.length != numInputArrays){
              throw new IllegalArgumentException("Invalid number of input arrays: network has " + numInputArrays
                      + " inputs, got " + features.length + " input arrays");
          }
          for( int i=0; i<layerIndexes.length; i++ ) {
              if(layerIndexes[i] < 0 || layerIndexes[i] >= topologicalOrder.length) {
                  throw new IllegalArgumentException("Invalid input index - index must be >= 0 and < " + topologicalOrder.length
                          + ", got index " + layerIndexes[i]);
              }
          }
          setInputs(features);
          setLayerMaskArrays(fMask, lMasks);
  
          MemoryWorkspace outputPrevious = null;
          if(outputWorkspace == null || outputWorkspace instanceof DummyWorkspace) {
              //Verify that no workspace is open externally
              WorkspaceUtils.assertNoWorkspacesOpen("Expected no workspace active before call to outputOfLayersDetached");
          } else {
              Preconditions.checkState(outputWorkspace.isScopeActive(), "Workspace \"" + outputWorkspace.getId() +
                      "\" was provided for the network/layer outputs. When provided, this workspace must be opened before " +
                      "calling the output method; furthermore, closing the workspace is the responsibility of the user");
              outputPrevious = outputWorkspace.getParentWorkspace();
          }
  
  
          //First: for each vertex, determine the highest index of the vertex that consumes it's output
          //Then: for each vertex, determine the forward pass step that each vertex's output has been fully consumed on
          //In other words, if vertex X -> Y and X -> Z, and topological sort order is X,a,Y,b,Z,
          //Then we know X's output activations have been fully consumed by step index 4 in the topological sort
          //thus vertexOutputsFullyConsumedByStep[X.index] = IndexOf(topologicalSort, Z.index)
  
          //Position in array: index of vertex. Value at position: the step (in topological order) that the activations
          // have been consumed by
          //Put another way: this is the step that it's safe to deallocate the layer's activations by closing the
          // corresponding workspace
          int[] vertexOutputsFullyConsumedByStep = new int[topologicalOrder.length];
          for(GraphVertex gv : vertices){
              int idx = gv.getVertexIndex();
              int maxStepOfOutputTo = -1;
              VertexIndices[] outputsTo = gv.getOutputVertices();
              if(outputsTo != null) {
                  //May be null for final/output layers
                  for (VertexIndices vi : outputsTo) {
                      int posInTopoSort = ArrayUtils.indexOf(topologicalOrder, vi.getVertexIndex());
                      if (posInTopoSort == -1) {
                          throw new IllegalStateException("Did not find vertex " + vi.getVertexIndex() + " in topological sort array");
                      }
                      maxStepOfOutputTo = Math.max(maxStepOfOutputTo, posInTopoSort);
                  }
              } else {
                  maxStepOfOutputTo = topologicalOrder.length-1;
              }
              vertexOutputsFullyConsumedByStep[idx] = maxStepOfOutputTo;
          }
  
          //Do forward pass according to the topological ordering of the network
          INDArray[] outputs = new INDArray[layerIndexes.length];
          int stopIndex = -1;
          for( int i=0; i<layerIndexes.length; i++ ){
              stopIndex = Math.max(stopIndex, ArrayUtils.indexOf(topologicalOrder, layerIndexes[i]));
          }
          List<LayerWorkspaceMgr> allWorkspaceManagers = new ArrayList<>();
          List<LayerWorkspaceMgr> freeWorkspaceManagers = new ArrayList<>();  //Basically used as a stack
          Map<MemoryWorkspace, LayerWorkspaceMgr> openActivationsWorkspaces = new IdentityHashMap<>();
  
          WorkspaceMode wsm = (train ? configuration.getTrainingWorkspaceMode() : configuration.getInferenceWorkspaceMode());
          boolean noWS = wsm == WorkspaceMode.NONE;
          LayerWorkspaceMgr allNone = noWS ? LayerWorkspaceMgr.noWorkspaces(helperWorkspaces) : null;
          List<MemoryWorkspace>[] closeAtEndIteraton = (List<MemoryWorkspace>[])new List[topologicalOrder.length];
          MemoryWorkspace initialWorkspace = Nd4j.getMemoryManager().getCurrentWorkspace();
          try {
              for (int i = 0; i <= stopIndex; i++) {
                  GraphVertex current = vertices[topologicalOrder[i]];
                  String vName = current.getVertexName();
                  int vIdx = current.getVertexIndex();
  
                  //First: determine what workspace manager we should use for forward pass in this vertex
                  LayerWorkspaceMgr workspaceMgr;
                  if (noWS) {
                      workspaceMgr = allNone;
                  } else {
                      //First: is there a free forward pass 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, wsName, WS_LAYER_ACT_X_CONFIG)
                                  .with(ArrayType.ACTIVATIONS, wsName, WS_LAYER_ACT_X_CONFIG)
                                  .with(ArrayType.FF_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)
                                  .build();
  
                          if(detachedInputs){
                              //Sometimes (like: external errors use cases) we don't want the activations/inputs to be
                              // in a workspace
                              workspaceMgr.setScopedOutFor(ArrayType.INPUT);
                              workspaceMgr.setScopedOutFor(ArrayType.ACTIVATIONS);
                          } else {
                              //Don't leverage out of async MultiDataSetIterator workspaces
                              if(features[0].isAttached()){
                                  workspaceMgr.setNoLeverageOverride(features[0].data().getParentWorkspace().getId());
                              }
                          }
  
                          allWorkspaceManagers.add(workspaceMgr);
                      }
                  }
                  workspaceMgr.setHelperWorkspacePointers(helperWorkspaces);
  
                  //Is this one of the layers/vertices that we want the output for?
                  boolean isRequiredOutput = false;
                  String origWSAct = null;
                  WorkspaceConfiguration origWSActConf = null;
                  if (ArrayUtils.contains(layerIndexes, vIdx)) {
                      isRequiredOutput = true;
  
                      if(outputWorkspace != null && !(outputWorkspace instanceof DummyWorkspace)){
                          //Place activations in user-specified workspace
                          origWSAct = workspaceMgr.getWorkspaceName(ArrayType.ACTIVATIONS);
                          origWSActConf = workspaceMgr.getConfiguration(ArrayType.ACTIVATIONS);
                          workspaceMgr.setWorkspace(ArrayType.ACTIVATIONS, outputWorkspace.getId(), outputWorkspace.getWorkspaceConfiguration());
                      } else {
                          //Standard case
                          if (!workspaceMgr.isScopedOut(ArrayType.ACTIVATIONS)) {
                              //Activations/output to return: don't want this in any workspace
                              origWSAct = workspaceMgr.getWorkspaceName(ArrayType.ACTIVATIONS);
                              origWSActConf = workspaceMgr.getConfiguration(ArrayType.ACTIVATIONS);
                              workspaceMgr.setScopedOutFor(ArrayType.ACTIVATIONS);
                          }
                      }
                  }
  
                  //Open the relevant workspace for the activations.
                  //Note that this will be closed only once the current vertex's activations have been consumed
                  MemoryWorkspace wsActivations = null;
                  if(outputWorkspace == null || outputWorkspace instanceof DummyWorkspace || !isRequiredOutput ){    //Open WS if (a) no external/output WS (if present, it's already open), or (b) not being placed in external/output WS
                      wsActivations = workspaceMgr.notifyScopeEntered(ArrayType.ACTIVATIONS);
                      openActivationsWorkspaces.put(wsActivations, workspaceMgr);
                  }
  
                  //Note that because we're opening activation 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
                  if(wsActivations != null )
                      wsActivations.setPreviousWorkspace(initialWorkspace);
  
                  int closeableAt = vertexOutputsFullyConsumedByStep[vIdx];
                  if(outputWorkspace == null || outputWorkspace instanceof DummyWorkspace || (wsActivations != null && !outputWorkspace.getId().equals(wsActivations.getId()))) {
                      if (closeAtEndIteraton[closeableAt] == null) {
                          closeAtEndIteraton[closeableAt] = new ArrayList<>();
                      }
                      closeAtEndIteraton[closeableAt].add(wsActivations);
                  }
  
  
                  try (MemoryWorkspace wsFFWorking = workspaceMgr.notifyScopeEntered(ArrayType.FF_WORKING_MEM)) {
                      VertexIndices[] inputsTo = current.getOutputVertices();
  
                      INDArray out;
                      if (current.isInputVertex()) {
                          out = features[vIdx];
                      } else {
  
                          if(fwdPassType == FwdPassType.STANDARD){
                              //Standard feed-forward case
                              out = current.doForward(train, workspaceMgr);
                          } else if(fwdPassType == FwdPassType.RNN_TIMESTEP){
                              if (current.hasLayer()) {
                                  //Layer
                                  INDArray input = current.getInputs()[0];
                                  Layer l = current.getLayer();
                                  if (l instanceof RecurrentLayer) {
                                      out = ((RecurrentLayer) l).rnnTimeStep(reshapeTimeStepInput(input), workspaceMgr);
                                  } else if(l instanceof org.deeplearning4j.nn.layers.wrapper.BaseWrapperLayer && ((org.deeplearning4j.nn.layers.wrapper.BaseWrapperLayer)l).getUnderlying() instanceof RecurrentLayer){
                                      RecurrentLayer rl = ((RecurrentLayer) ((org.deeplearning4j.nn.layers.wrapper.BaseWrapperLayer)l).getUnderlying());
                                      out = rl.rnnTimeStep(reshapeTimeStepInput(input), workspaceMgr);
                                  } else if (l instanceof MultiLayerNetwork) {
                                      out = ((MultiLayerNetwork) l).rnnTimeStep(reshapeTimeStepInput(input));
                                  } else {
                                      //non-recurrent layer
                                      out = current.doForward(train, workspaceMgr);
                                  }
                              } else {
                                  //GraphNode
                                  out = current.doForward(train, workspaceMgr);
                              }
                          } else {
                              throw new IllegalArgumentException("Unsupported forward pass type for this method: " + fwdPassType);
                          }
                          validateArrayWorkspaces(workspaceMgr, out, ArrayType.ACTIVATIONS, vName, false, "Feed forward (inference)");
                      }
  
                      if(inputsTo != null) {  //Output vertices may not input to any other vertices
                          for (VertexIndices v : inputsTo) {
                              //Note that we don't have to do anything special here: the activations are always detached in
                              // this method
                              int inputToIndex = v.getVertexIndex();
                              int vIdxEdge = v.getVertexEdgeNumber();
                              vertices[inputToIndex].setInput(vIdxEdge, out, workspaceMgr);
                          }
                      }
  
                      if(clearLayerInputs) {
                          current.clear();
                      }
  
                      if(isRequiredOutput){
                          outputs[ArrayUtils.indexOf(layerIndexes, vIdx)] = out;
                          if(origWSAct != null){
                              //Reset the configuration, as we may reuse this workspace manager...
                              workspaceMgr.setWorkspace(ArrayType.ACTIVATIONS, origWSAct, origWSActConf);
                          }
                      }
                  }
  
                  //Close any activations workspaces that we no longer require
                  //Note that activations workspaces can be closed only once the corresponding output activations have
                  // been fully consumed
                  if(closeAtEndIteraton[i] != null){
                      for(MemoryWorkspace wsAct : closeAtEndIteraton[i]){
                          wsAct.close();
                          LayerWorkspaceMgr canNowReuse = openActivationsWorkspaces.remove(wsAct);
                          freeWorkspaceManagers.add(canNowReuse);
                      }
                  }
              }
          } finally {
              //Close all open workspaces... usually this list will be empty, but not if an exception is thrown
              //Though if stopIndex < numLayers, some might still be open
              for(MemoryWorkspace ws : openActivationsWorkspaces.keySet()){
                  while (ws.isScopeActive()) {
                      //Edge case here: seems that scoping out can increase the tagScope of the current WS
                      //and if we hit an exception during forward pass, we aren't guaranteed to call close a sufficient
                      // number of times to actually close it, in all cases
                      ws.close();
                  }
              }
              Nd4j.getMemoryManager().setCurrentWorkspace(initialWorkspace);
  
              if(outputWorkspace == null || outputWorkspace instanceof DummyWorkspace) {
                  WorkspaceUtils.assertNoWorkspacesOpen("Expected no workspace active at the end of outputOfLayerDetached");
              } else {
                  Preconditions.checkState(outputWorkspace.isScopeActive(), "Expected output workspace to still be open" +
                          "at end of outputOfLayerDetached, but ");
                  outputWorkspace.setPreviousWorkspace(outputPrevious);
              }
          }
  
          return outputs;
      }