docs: def step(self, loss, optimizer, scheduler, update=True):
          """
          Performs one step of the optimizer.
          Applies loss scaling, computes gradients in fp16, converts gradients to
          fp32, inverts scaling and applies optional gradient norm clipping.
          If gradients are finite, it applies update to fp32 master weights and
          copies updated parameters to fp16 model for the next iteration. If
          gradients are not finite, it skips the batch and adjusts scaling factor
          for the next iteration.
  
          :param loss: value of loss function
          :param optimizer: optimizer
          :param update: if True executes weight update
          """
          loss *= self.loss_scale
          loss.backward()
  
          if update:
              self.set_grads(self.fp32_params, self.fp16_model.parameters())
              if self.loss_scale != 1.0:
                  for param in self.fp32_params:
                      param.grad.data /= self.loss_scale
  
              norm = clip_grad_norm_(self.fp32_params, self.grad_clip)
  
              if math.isfinite(norm):
                  scheduler.step()
                  optimizer.step()
                  self.set_weights(self.fp16_model.parameters(),
                                   self.fp32_params)
                  self.since_last_invalid += 1
              else:
                  self.loss_scale /= self.dls_downscale
                  self.since_last_invalid = 0
                  logging.info(f'Gradient norm: {norm}')
                  logging.info(f'Skipped batch, new scale: {self.loss_scale}')
  
              if self.since_last_invalid >= self.dls_upscale_interval:
                  self.loss_scale *= self.dls_upscale
                  self.loss_scale = min(self.loss_scale, 8192.0)
                  logging.info(f'Upscaling, new scale: {self.loss_scale}')
                  self.since_last_invalid = 0
  
              self.fp16_model.zero_grad()