docs: public void init(INDArray parameters, boolean cloneParametersArray) {
          if (initCalled)
              return;
  
          DataType netDtype = getConfiguration().getDataType();
          if(parameters != null && parameters.dataType() != netDtype){
              if(cloneParametersArray){
                  try(MemoryWorkspace ws = Nd4j.getWorkspaceManager().scopeOutOfWorkspaces()) {
                      parameters = parameters.castTo(netDtype);
                  }
              } else {
                  throw new IllegalStateException("Error initializing network: Network datatype is set to " + netDtype
                          + " but provided array has datatype " + parameters.dataType() + " with cloneParametersArray argument" +
                          " set to false. Cannot initialize net with specified datatype array if that array does not match network datatype");
              }
          }
  
          if (configuration.getTrainingWorkspaceMode() == null)
              configuration.setTrainingWorkspaceMode(WorkspaceMode.NONE);
  
          if (configuration.getInferenceWorkspaceMode() == null)
              configuration.setInferenceWorkspaceMode(WorkspaceMode.NONE);
  
          if (configuration.getCacheMode() == null)
              configuration.setCacheMode(CacheMode.NONE);
  
          OneTimeLogger.info(log, "Starting ComputationGraph with WorkspaceModes set to [training: {}; inference: {}], cacheMode set to [{}]",
                  configuration.getTrainingWorkspaceMode(), configuration.getInferenceWorkspaceMode(), configuration.getCacheMode());
  
          //First: build topological ordering, based on configuration. Used for forward pass, backprop and order of parameters/gradients
          GraphIndices indices = calculateIndices();
          topologicalOrder = indices.getTopologicalSortOrder();
  
          //Initialization: create the GraphVertex objects, based on configuration structure
          Map<String, org.deeplearning4j.nn.conf.graph.GraphVertex> configVertexMap = configuration.getVertices();
  
          //Names of all of the (data) inputs to the ComputationGraph
          List<String> networkInputNames = configuration.getNetworkInputs();
  
          //Inputs for each layer and GraphNode:
          Map<String, List<String>> vertexInputs = configuration.getVertexInputs();
          this.vertices = new GraphVertex[networkInputNames.size() + configuration.getVertices().size()];
  
          //All names: inputs, layers and graph nodes (index to name map)
          Map<String, Integer> allNamesReverse = new HashMap<>();
  
          //Create network input vertices:
          int vertexNumber = 0;
          for (String name : networkInputNames) {
              GraphVertex gv = new InputVertex(this, name, vertexNumber, null, netDtype); //Output vertices: set later
              allNamesReverse.put(name, vertexNumber);
              vertices[vertexNumber++] = gv;
          }
  
          //Go through layers, and work out total number of parameters. Then allocate full parameters array
          long numParams = 0;
          long[] numParamsForVertex = new long[topologicalOrder.length];
          int i = 0;
          for (; i < configuration.getNetworkInputs().size(); i++) {
              numParamsForVertex[i] = 0; //No parameters for input vertices
          }
          for(; i<topologicalOrder.length; i++ ){
              String name = indices.getIdxToName().get(i);
              org.deeplearning4j.nn.conf.graph.GraphVertex n = configVertexMap.get(name);
              numParamsForVertex[i] = n.numParams(true);
              numParams += numParamsForVertex[i];
          }
  
          boolean initializeParams;
          if (parameters != null) {
              if (!parameters.isRowVectorOrScalar())
                  throw new IllegalArgumentException("Invalid parameters: should be a row vector");
              if (parameters.length() != numParams)
                  throw new IllegalArgumentException("Invalid parameters: expected length " + numParams + ", got length "
                          + parameters.length());
  
              if (cloneParametersArray)
                  flattenedParams = parameters.dup();
              else
                  flattenedParams = parameters;
  
              initializeParams = false;
          } else if(numParams > 0){
              flattenedParams = Nd4j.create(netDtype, 1, numParams);
              initializeParams = true;
          } else {
              flattenedParams = null;
              initializeParams = false;
          }
  
          //Set RNG seed, for repeatability between initializations when set
          if (initializeParams) {
              Nd4j.getRandom().setSeed(conf().getSeed());
          }
  
          //Given the topological ordering: work out the subset of the parameters array used for each layer
          // Then extract out for use when initializing the Layers
          INDArray[] paramsViewForVertex = new INDArray[topologicalOrder.length];
          long paramOffsetSoFar = 0;
          i = 0;
          for (int vertexIdx : topologicalOrder) {
              long nParamsThisVertex = numParamsForVertex[vertexIdx];
              if (nParamsThisVertex != 0) {
                  paramsViewForVertex[vertexIdx] = flattenedParams.get(NDArrayIndex.interval(0,0,true),
                          NDArrayIndex.interval(paramOffsetSoFar, paramOffsetSoFar + nParamsThisVertex));
              }
              i++;
              paramOffsetSoFar += nParamsThisVertex;
          }
  
  
          int numLayers = 0;
          List<Layer> tempLayerList = new ArrayList<>();
          defaultConfiguration.clearVariables();
          List<String> variables = defaultConfiguration.variables(false);
          i = configuration.getNetworkInputs().size();
          for(; i<topologicalOrder.length; i++ ){
              String name = indices.getIdxToName().get(i);
              org.deeplearning4j.nn.conf.graph.GraphVertex n = configVertexMap.get(name);
  
              GraphVertex gv = n.instantiate(this, name, vertexNumber, paramsViewForVertex[vertexNumber],
                      initializeParams, netDtype);
  
              if(gv == null){
                  throw new IllegalStateException("Encountered null layer/vertex during initialization for layer \"" + name +
                          "\": " + n.getClass().getSimpleName() + " initialization returned null layer/vertex?");
              }
  
              if (gv.hasLayer()) {
                  numLayers++;
                  Layer l = gv.getLayer();
                  tempLayerList.add(l);
                  List<String> layerVariables = l.conf().variables();
                  if (layerVariables != null) {
                      for (String s : layerVariables) {
                          variables.add(gv.getVertexName() + "_" + s);
                      }
                  }
              }
  
              allNamesReverse.put(name, vertexNumber);
              vertices[vertexNumber++] = gv;
          }
          layers = tempLayerList.toArray(new Layer[numLayers]);
  
          //Create the lookup table, so we can find vertices easily by name
          verticesMap = new HashMap<>();
          for (GraphVertex gv : vertices) {
              verticesMap.put(gv.getVertexName(), gv);
          }
  
          //Now: do another pass to set the input and output indices, for each vertex
          // These indices are used during forward and backward passes
          //To get output indices: need to essentially build the graph in reverse...
          Map<String, List<String>> verticesOutputTo = new HashMap<>(); //Key: vertex. Values: vertices that this node is an input for
          for (GraphVertex gv : vertices) {
              String vertexName = gv.getVertexName();
              List<String> vertexInputNames;
              vertexInputNames = vertexInputs.get(vertexName);
  
              if (vertexInputNames == null)
                  continue;
  
              //Build reverse network structure:
              for (String s : vertexInputNames) {
                  List<String> list = verticesOutputTo.get(s);
                  if (list == null) {
                      list = new ArrayList<>();
                      verticesOutputTo.put(s, list);
                  }
                  list.add(vertexName); //Edge: s -> vertexName
              }
          }
  
  
          for (GraphVertex gv : vertices) {
              String vertexName = gv.getVertexName();
              int vertexIndex = gv.getVertexIndex();
              List<String> vertexInputNames;
              vertexInputNames = vertexInputs.get(vertexName);
  
              if (vertexInputNames == null)
                  continue;
  
              VertexIndices[] inputIndices = new VertexIndices[vertexInputNames.size()];
              for (int j = 0; j < vertexInputNames.size(); j++) {
                  String inName = vertexInputNames.get(j);
                  int inputVertexIndex = allNamesReverse.get(inName);
  
                  //Here: we have x -> gv connection
                  //gv might have multiple inputs, not just x
                  //Need to know which input x is
                  int inputNumber = vertexInputs.get(vertexName).indexOf(inName);
  
                  if (inputNumber == -1)
                      throw new IllegalStateException("Could not find vertex " + vertexIndex + " in the list of inputs "
                              + "for vertex " + gv.getVertexName() + "; error in graph structure?");
  
                  inputIndices[j] = new VertexIndices(inputVertexIndex, inputNumber);
              }
  
              gv.setInputVertices(inputIndices);
          }
  
          //Handle the outputs for this vertex
          for (GraphVertex gv : vertices) {
              String vertexName = gv.getVertexName();
  
              List<String> thisVertexOutputsTo = verticesOutputTo.get(vertexName);
  
              if (thisVertexOutputsTo == null || thisVertexOutputsTo.isEmpty())
                  continue; //Output vertex
              VertexIndices[] outputIndices = new VertexIndices[thisVertexOutputsTo.size()];
              int j = 0;
              for (String s : new HashSet<>(thisVertexOutputsTo)) {
                  //First, we have gv -> s
                  //Which input in s does gv connect to? s may in general have multiple inputs...
                  List<String> nextVertexInputNames = vertexInputs.get(s);
  
                  for (int k = 0; k < nextVertexInputNames.size(); k++) {
                      if(vertexName.equals(nextVertexInputNames.get(k))){
                          int outputVertexIndex = allNamesReverse.get(s);
                          outputIndices[j++] = new VertexIndices(outputVertexIndex, k);
                      }
                  }
              }
              gv.setOutputVertices(outputIndices);
          }
  
          //Mark any output vertices as outputs:
          for (String s : configuration.getNetworkOutputs()) {
              GraphVertex gv = verticesMap.get(s);
              gv.setOutputVertex(true);
          }
  
          // now we init solver & optimizer
          if (solver == null) {
              try (MemoryWorkspace wsO = Nd4j.getMemoryManager().scopeOutOfWorkspaces()) {
                  solver = new Solver.Builder().configure(conf()).listeners(getListeners()).model(this).build();
                  solver.initOptimizer();
              }
          }
  
          //Mark which layers can safely modify their input in-place. This is a performance optimization for
          // dropout and similar operations.
          // Safe when the input is: (a) it's not a graph input, and (b) isn't shared by any other layers/vertices
  
          Map<String,List<String>> seenAsInputTo = new HashMap<>();
          for(Map.Entry<String,List<String>> entry : configuration.getVertexInputs().entrySet()){
              for(String s : entry.getValue() ){
                  if (!seenAsInputTo.containsKey(s)) {
                      seenAsInputTo.put(s, new ArrayList<String>());
                  }
                  List<String> seen = seenAsInputTo.get(s);
                  seen.add(entry.getKey());
              }
          }
  
          for(Layer l : layers){
              String layerName = l.conf().getLayer().getLayerName();
              List<String> inputs = configuration.getVertexInputs().get(layerName);
              String in = inputs.get(0);  //For now: layers should have exactly 1 input
  
              if(configuration.getNetworkInputs().contains(in)){
                  //TODO When is it safe to NOT allow input modifucation? It's not always safe...
                  // For example dropout + iterating over List<MultiDataSet> that is used for multiple epochs...
                  continue;
              }
  
              List<String> seen = seenAsInputTo.get(in);
              if(seen.size() == 1){
                  l.allowInputModification(true);
              } else {
                  //For the count > 1 case, we can work out if it's the last one in the topological order... at which point,
                  // it should be safe to use
                  int thisIdx = indices.getNameToIdx().get(layerName);
                  int thisTopoPos = ArrayUtils.indexOf(indices.getTopologicalSortOrder(), thisIdx);
                  int maxTopoPosition = -1;
                  for(String s : seen){
                      int idx = indices.getNameToIdx().get(s);
                      int topoPos = ArrayUtils.indexOf(indices.getTopologicalSortOrder(), idx);
                      maxTopoPosition = Math.max(maxTopoPosition, topoPos);
                  }
  
                  if(thisTopoPos == maxTopoPosition){
                      //Last one in the topo sort... all other layers have already consumed this input by the time this layer's
                      // forward pass is done
                      l.allowInputModification(true);
                  }   //Otherwise: keep default of false
              }
          }
  
          synchronizeIterEpochCounts();
          initCalled = true;
      }