docs: public synchronized void update(Gradient gradient, int iteration, int epoch, int batchSize, LayerWorkspaceMgr workspaceMgr) {
  
          //First: check if gradient is standard or external...
          //In a MultiLayerNetwork, the INDArray returned by .gradient() is always the standard full view array
          // hence should be the same object under normal circumstances
          boolean isExternal = gradient.gradient() != getFlattenedGradientsView();
  
          //Split up the gradients on a per-layer basis, for pre-apply
          Map<String, Gradient> layerGradients = new HashMap<>();
  
          Trainable[] layers = getOrderedLayers();
          if (layers.length == 1 && isSingleLayerUpdater()) {
              layerGradients.put(layers[0].getConfig().getLayerName(), gradient);
          } else {
              for (Map.Entry<String, INDArray> gradientPair : gradient.gradientForVariable().entrySet()) {
                  String key = gradientPair.getKey();
                  int idx = key.lastIndexOf('_');
                  if (idx == -1)
                      throw new IllegalStateException(
                                      "Invalid key: Gradient key does not have layer separator: \"" + key + "\"");
                  String layerName = key.substring(0, idx);
  
                  Gradient g = layerGradients.get(layerName);
                  if (g == null) {
                      g = new DefaultGradient();
                      layerGradients.put(layerName, g);
                  }
  
                  String newKey = key.substring(idx + 1);
                  g.setGradientFor(newKey, gradientPair.getValue());
              }
          }
  
          if(isMiniBatch()){
              divideByMinibatch(isExternal, gradient, batchSize);
          }
  
          //PRE apply (gradient clipping, etc): done on a per-layer basis
          for (Map.Entry<String, Gradient> entry : layerGradients.entrySet()) {
              String layerName = entry.getKey();
              Trainable layer = layersByName.get(layerName);
  
              preApply(layer, layerGradients.get(layerName), iteration);
          }
  
          //Apply the updaters in blocks. This also applies LR and momentum schedules, L1 and L2
          if(getClass() != LayerUpdater.class){
              //OK for LayerUpdater as this is part of layerwise pretraining
              workspaceMgr.assertNotOpen(ArrayType.UPDATER_WORKING_MEM, "Updater working memory");
          }
          for (UpdaterBlock ub : updaterBlocks) {
              if (ub.skipDueToPretrainConfig(this instanceof LayerUpdater)) {
                  //Should skip some updater blocks sometimes
                  //For example, VAE decoder params while doing supervised backprop
                  continue;
              }
              try(MemoryWorkspace ws = workspaceMgr.notifyScopeEntered(ArrayType.UPDATER_WORKING_MEM)){
                  if (isExternal) {
                      //RL4J etc type case: calculate gradients in 1 net, update them in another
                      ub.updateExternalGradient(iteration, epoch, gradient.gradient(), getParams());
                  } else {
                      //Standard case
                      ub.update(iteration, epoch);
                  }
              }
          }
      }