pikaliov

horovod_train

Sep 4th, 2020 (edited)
453
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 22.18 KB | None | 0 0
  1. import glob
  2. import horovod.tensorflow as hvd  # isort:skip
  3. from tqdm import tqdm
  4. #import tensorflow_addons as tfa
  5.  
  6. from src.model_mod.opt import LAMB, AdamWeightDecay, WarmUp
  7. from src.model_mod.lookahead_opt import Lookahead
  8. from src.prep_data_mod.create_pretraining_data import GeneratorBERT
  9. from src.prep_data_mod.build_dataset import Dataset
  10. from src.confs.base_conf import logger_obj, fdir_obj, input_obj
  11. from src.model_mod.transformer_lm_models.transformer_model_lm_pre import TransfomerMaskedLM
  12. from src.confs.nn_config import NNConfig
  13. from src.confs.training_config import TrainConfig
  14. from src.model_mod.ga import GradientAccumulator
  15. from src.utils_mod.train_utils import read_meta_data_csv, fill_meta_data_csv, write_meta_data_csv
  16.  
  17.  
  18.  
  19. def mlm_loss_fn(logits, label_ids, label_weights, voc_size):
  20.     """ function to compute loss and acc based on logits results
  21.        Args:
  22.            logits
  23.            labels_ids
  24.            label_weights
  25.            voc_size
  26.        Returns:
  27.            lm_loss
  28.            acc
  29.    """
  30.     logits = tf.nn.log_softmax(logits, axis=-1) # get log_
  31.     lm_label_ids_one_hot = tf.keras.backend.one_hot(label_ids, voc_size)
  32.     #lm_label_weights = tf.keras.backend.reshape(label_weights, [-1])
  33.     lm_per_example_loss = -tf.keras.backend.sum(logits * lm_label_ids_one_hot, axis=[-1])
  34.     loss_numerator = tf.keras.backend.sum(label_weights * lm_per_example_loss)
  35.     denominator = tf.keras.backend.sum(label_weights) + 1e-5
  36.     preds = tf.math.argmax(logits, -1)
  37.     preds = tf.cast(preds, dtype=tf.int32)
  38.     acc_per_token = label_weights * tf.cast(tf.math.equal(label_ids, preds), tf.float32)  # [b, num_tokens]
  39.     acc_numerator = tf.reduce_sum(acc_per_token)
  40.     lm_loss = loss_numerator / denominator
  41.     acc = acc_numerator / denominator
  42.     return lm_loss, acc
  43.  
  44. class PreTrainAlbertLM():
  45.     """
  46.    model_name,
  47.    albert_config,
  48.    train_config,
  49.    load_model=False,
  50.    load_weights=False,
  51.    checkpoint_path=None
  52.    """
  53.     def __init__(self, model_name, albert_config, train_config, load_optimizer=False):
  54.         self.config = train_config  # training config
  55.         self.model_name = model_name    # model_name
  56.         self.load_optimizer = load_optimizer    # load optimizer or not
  57.         self.voc_size = albert_config.voc_size  # vocabulary size
  58.         # path to model directory to hold output results
  59.         self.model_dir = fdir_obj.path2maindir + '/models/{}/'.format(model_name)
  60.         # path 2 store checkpoints of model
  61.         self.path2model_ckpt = self.model_dir + '/ckpt/'
  62.         self.logger = tf.get_logger()   # logger_object
  63.         assert self.config.optimizer in ["adamw", "lamb"], \
  64.             logger_obj.logger.error('The optimizer type is not supported: %s' % self.config.optimizer)
  65.  
  66.         # value for gradient clipping
  67.         self.max_grad_norm = self.config.max_grad_norm
  68.         # max steps to train
  69.         self.total_train_steps = self.config.total_train_steps
  70.         # number of warmup steps for linear loss
  71.         self.num_warmup_steps = self.config.num_warmup_steps
  72.         # number of accumulate gradients step
  73.         self.gradient_accumulation_steps = self.config.gradient_accumulation_steps
  74.         # compute global batches size
  75.         self.global_train_batch_size = self.config.train_batch_size * hvd.size() * self.gradient_accumulation_steps
  76.         self.global_test_batch_size = self.config.test_batch_size * hvd.size() * self.gradient_accumulation_steps
  77.         self.voc_size = albert_config.voc_size  # size of vocab with tokens
  78.         self.max_seq_length = input_obj.max_num_phrase  # maximum length
  79.         self.path2metadata = self.model_dir + '/info/'   # directory to output files with information of train
  80.         # fils to hold result info about training proc
  81.         self.meta_test_sum = self.path2metadata + 'test_summary.csv'
  82.         self.meta_test_av = self.path2metadata + 'test_average.csv'
  83.         self.meta_train_sum = self.path2metadata + 'train_summary.csv'
  84.         self.meta_train_av = self.path2metadata + 'train_average.csv'
  85.         self.meta_csv = self.path2metadata + "meta.csv"
  86.         # number steps for one epoch
  87.         self.train_steps_per_epoch = math.ceil(self.total_train_steps / self.config.num_epochs)
  88.         #self.steps_for_save = math.ceil(self.train_steps_per_epoch / self.freq_save)
  89.         self.path2tb = self.path2metadata + '/tb/'  # path to tensorboadd directory
  90.         # arr drrs to hold results
  91.         arr_dirs = [self.model_dir, self.path2model_ckpt, self.path2metadata, self.path2tb]
  92.         # create dir trees if absent
  93.         for dir in arr_dirs:
  94.             if not Path(dir).is_dir():
  95.                 Path(dir).mkdir(parents=True, exist_ok=True)
  96.         # initialize tensorboard
  97.         self.summary_writer = tf.summary.create_file_writer(self.path2tb)
  98.         # initaalize csv files to hold result
  99.         fill_meta_data_csv(meta_train_sum=self.meta_train_sum, meta_test_sum=self.meta_test_sum,
  100.                            meta_test_av=self.meta_test_av, meta_train_av=self.meta_train_av)
  101.         self.loaded_optimizer_weights = None    # object to hold optimizer weights
  102.         self.model = TransfomerMaskedLM(config=albert_config)  # our model
  103.         # initialize learning rate schedule
  104.         learning_rate_fn = tf.keras.optimizers.schedules.PolynomialDecay(
  105.                 initial_learning_rate=self.config.initial_lr * hvd.size(),
  106.                 decay_steps=int(self.total_train_steps),
  107.                 end_learning_rate=0.0)
  108.         learning_rate_fn = WarmUp(initial_learning_rate=self.config.initial_lr * hvd.size(),
  109.                                   decay_schedule_fn=learning_rate_fn,
  110.                                   warmup_steps=self.num_warmup_steps)
  111.         # create optimizer
  112.         if self.config.optimizer == "lamb":
  113.             optimizer_fn = LAMB
  114.         else:
  115.             optimizer_fn = AdamWeightDecay
  116.         self.optimizer = optimizer_fn(learning_rate=learning_rate_fn, weight_decay_rate=self.config.weight_decay,
  117.                                       beta_1=self.config.opt_beta1, beta_2=self.config.opt_beta2,
  118.                                       epsilon=self.config.opt_epsilon,
  119.                                       exclude_from_weight_decay=["layer_norm", "bias"],
  120.                                       experimental_aggregate_gradients=False)
  121.         #if self.config.use_lookahead_opt:
  122.         self.optimizer = Lookahead(self.optimizer, sync_period=6, slow_step_size=0.5,
  123.                                    experimental_aggregate_gradients=False)
  124.  
  125.         self.optimizer = tf.train.experimental.enable_mixed_precision_graph_rewrite(self.optimizer, loss_scale="dynamic")
  126.         self.optimizer._HAS_AGGREGATE_GRAD = False
  127.         # create gradient accumulator object
  128.         self.gradient_accumulator = GradientAccumulator()
  129.         self.ckpt = tf.train.Checkpoint(model=self.model, optimizer=self.optimizer)
  130.         self.ckpt_manager = tf.train.CheckpointManager(self.ckpt, directory=self.path2model_ckpt,
  131.                                                              max_to_keep=100)
  132.         self.first_batch = True
  133.         # print out info about training parameters
  134.         logger_obj.logger.info('Num epochs:{}\nTrain batch size:{}\n'
  135.                                'Global train batch size:{}\n'
  136.                                'Train steps per epoch:{}\n'
  137.                                'Num train step:{}\n Num warmup steps:{}\n'
  138.                                'Optimizer:{}\n'.format(self.config.num_epochs,
  139.                                                        self.config.train_batch_size,
  140.                                                        self.global_train_batch_size,
  141.                                                        self.train_steps_per_epoch,
  142.                                                        self.total_train_steps,
  143.                                                        self.num_warmup_steps,
  144.                                                        self.config.optimizer))
  145.  
  146.     def allreduce(self, mlm_loss, mlm_acc):
  147.         """
  148.        function to compile gradients and using horovod functional to output res
  149.        Args:
  150.            mlm_loss: loss per gpu
  151.            mlm_acc: accuracy per gpu
  152.        Returns:
  153.            mlm_loss: tf.float: val loss
  154.            mlm_acc: tf.float: val accuracy
  155.            grad_norm: tf.float: norm values of gradient
  156.            weight_norm: tf.float: norm values of weights
  157.        """
  158.         scaled_grads = self.gradient_accumulator.gradients
  159.         grads = self.optimizer.get_unscaled_gradients(scaled_grads)
  160.         # This, which is equivalent to sparse_as_dense=True, gives a mild 2% speedup
  161.         grads = [
  162.             tf.convert_to_tensor(grad)
  163.             if grad is not None and isinstance(grad, tf.IndexedSlices)
  164.             else grad
  165.             for grad in grads
  166.         ]
  167.         # !todo: Does placing this clip before or after allreduce affect accuracy?
  168.         # Placing before has a regularization effect, no single example can contribute as much.
  169.         # Placing before also gives a 20% speedup when training BERT-large, probably because the
  170.         # gradient operations can be fused by XLA.
  171.         if self.config.use_clip_grad:
  172.             (grads, grad_norm) = tf.clip_by_global_norm(grads, clip_norm=self.max_grad_norm)
  173.         else:
  174.             grad_norm = grads
  175.         weight_norm = tf.math.sqrt(tf.math.reduce_sum([tf.norm(var, ord=2) ** 2 for var in self.model.trainable_variables]))
  176.         grads = [
  177.             hvd.allreduce(grad, compression=hvd.Compression.fp16) if grad is not None else None
  178.             for grad in grads
  179.         ]
  180.         self.optimizer.apply_gradients(
  181.             [
  182.                 (tf.cast(grad, var.dtype), var)
  183.                 for (grad, var) in zip(grads, self.model.trainable_variables)
  184.                 if grad is not None
  185.             ], experimental_aggregate_gradients=False
  186.         )
  187.         # Clear the gradient accumulator
  188.         self.gradient_accumulator.reset()
  189.         mlm_loss = hvd.allreduce(mlm_loss)
  190.         mlm_acc = hvd.allreduce(mlm_acc)
  191.         return mlm_loss, mlm_acc, grad_norm, weight_norm
  192.  
  193.     def train_grad(self, input_ids, masked_lm_pos, label_ids, label_weights):
  194.         """
  195.        Args:
  196.            input_ids: masked tokens ids
  197.            masked_lm_pos: positions of masked tokens
  198.            label_ids: ids of masked tokens
  199.            label_weights: masked weights (1.0 if not masked, 0 if masked)
  200.        Returns:
  201.            mlm_loss: loss
  202.            mlm_acc: acc
  203.        """
  204.         with tf.GradientTape() as tape:
  205.             mlm_logits = self.model(input_ids, masked_lm_pos, training=True)
  206.             mlm_loss, mlm_acc = mlm_loss_fn(logits=mlm_logits,
  207.                                             label_ids=label_ids, label_weights=label_weights, voc_size=self.voc_size)
  208.             mlm_loss = tf.cast(mlm_loss, dtype=tf.float32)
  209.             scaled_loss = self.optimizer.get_scaled_loss(mlm_loss)
  210.         # !todo: On iteration 0, loss=11 and loss_scale()=32768, so scaled_loss=inf.
  211.         # But scaled_grads is not inf, how? tape.gradient() must not be using direct backprop calc
  212.         #tape = hvd.DistributedGradientTape(tape)
  213.         scaled_grads = tape.gradient(scaled_loss, self.model.trainable_variables)
  214.         self.gradient_accumulator(scaled_grads)
  215.         #self.optimizer.apply_gradients(zip(scaled_grads, self.model.trainable_variables))
  216.         if self.first_batch:
  217.             hvd.broadcast_variables(self.model.variables, root_rank=0)
  218.             hvd.broadcast_variables(self.optimizer.variables(), root_rank=0)
  219.             self.first_batch = False
  220.         return mlm_loss, mlm_acc
  221.  
  222.     def train_step(self, inputs):
  223.         """
  224.        functional of train step using input da of batches of batches
  225.        through horovod system
  226.        Args:
  227.            inputs: [grad_step, batch, seq_length] : input values of batch
  228.        Returns:
  229.            mlm_loss
  230.            mlm_acc
  231.            grad_norm
  232.            weight_norm
  233.        """
  234.         # initialize values to hold results throug batches of batches
  235.         total_mlm_loss, total_mlm_acc = (tf.constant(0, dtype=tf.float32), tf.constant(0, dtype=tf.float32),)
  236.         # loop of batches of batches
  237.         print(inputs)
  238.         for step in range(self.gradient_accumulation_steps):
  239.             input_ids, masked_lm_pos, label_ids, label_weights = inputs
  240.             input_ids = input_ids[step]
  241.             masked_lm_pos = masked_lm_pos[step]
  242.             label_ids = label_ids[step]
  243.             label_weights = label_weights[step]
  244.             mlm_loss, mlm_acc = self.train_grad(input_ids, masked_lm_pos, label_ids, label_weights)
  245.             total_mlm_loss += tf.cast(mlm_loss, total_mlm_loss.dtype)
  246.             total_mlm_acc += tf.cast(mlm_acc, total_mlm_acc.dtype)
  247.         total_mlm_loss /= self.gradient_accumulation_steps
  248.         total_mlm_acc /= self.gradient_accumulation_steps
  249.         return total_mlm_loss, total_mlm_acc #self.allreduce(mlm_loss=total_mlm_loss, mlm_acc=total_mlm_acc, )
  250.  
  251.     def test_step(self, inputs):
  252.         """validation step
  253.        Args:
  254.            inputs:
  255.        Returns:
  256.            loss, acc
  257.        """
  258.         input_word_ids, masked_positions, mlm_ids, label_weights = inputs
  259.         mlm_logits = self.model(input_word_ids=input_word_ids, masked_positions=masked_positions,
  260.                                 training=tf.convert_to_tensor(False))
  261.         loss, acc = mlm_loss_fn(logits=mlm_logits,
  262.                                 label_ids=mlm_ids, label_weights=label_weights, voc_size=self.voc_size)
  263.         return loss, acc
  264.  
  265.     def test_op(self, num_epoch):
  266.         """validation procedure
  267.        Args:
  268.            num_epoch: number of epoch (current)
  269.        Returns:
  270.            val_mlm_loss
  271.            val_mlm_acc
  272.        """
  273.         count = 0
  274.         val_mlm_loss, val_mlm_acc= (0, 0)
  275.         start_eval_time = time.time()
  276.         for (test_batch, (inputs)) in enumerate(self.test_dataset):
  277.             loss, acc = self.test_step(inputs)
  278.             val_mlm_loss += loss
  279.             val_mlm_acc += acc
  280.             loss_ = loss.numpy()
  281.             acc_ = acc.numpy()
  282.             count += 1
  283.             if test_batch % self.config.show_per_step == 0:
  284.                 logger_obj.logger.info(f'Test --> Epoch {num_epoch+1} | Batch {test_batch} | Loss {loss_:.4f} | Acc {acc_:4f}')
  285.             df_test_sum = pd.DataFrame({'epoch_num': [num_epoch], 'loss': [loss], 'acc' : [acc]})
  286.             df_test_sum.to_csv(self.meta_test_sum, sep='\t', index=False, columns=None, mode='a')
  287.         #
  288.         val_mlm_loss /= count
  289.         val_mlm_acc /= count
  290.         logger_obj.logger.info(f'Test --> Epoch {num_epoch+1} | Av Loss {val_mlm_loss:.4f} | Av Acc {val_mlm_acc:.4f}')
  291.         df_test_av = pd.DataFrame({'epoch_num': [num_epoch], 'av_loss': [val_mlm_loss], 'av_acc' : val_mlm_acc})
  292.         df_test_av.to_csv(self.meta_test_av, sep='\t', index=False, columns=None, mode='a')
  293.         end_eval_epoch_time = time.time()
  294.         logger_obj.logger.info(f'Time taken for test 1 epoch: {end_eval_epoch_time - start_eval_time} secs')
  295.         return val_mlm_loss, val_mlm_acc
  296.  
  297.     def train_op(self):
  298.         """
  299.        train procedure
  300.        """
  301.         # initialize generator function to generate input data
  302.         gen_train = GeneratorBERT(input_dir=fdir_obj.dir_train_data, random_seed=self.config.random_seed)
  303.         # and format input data from arrat to Tensorflow format
  304.         ds_train_obj = Dataset(generator=gen_train, batch_size=self.config.train_batch_size,
  305.                                buffer_size=self.config.train_batch_size, is_train=True, len2=input_obj.max_num_bpe_mask,
  306.                                )
  307.         self.train_dataset = ds_train_obj.next_element
  308.         if self.gradient_accumulation_steps > 1:
  309.             self.train_dataset = self.train_dataset.batch(self.gradient_accumulation_steps)
  310.             # One iteration with 10 dupes, 8 nodes seems to be 60-70k steps.
  311.             self.train_dataset = self.train_dataset.prefetch(buffer_size=8)
  312.         if input_obj.mode == "train_eval":
  313.             #
  314.             gen_test = GeneratorBERT(input_dir=fdir_obj.dir_dev_data, random_seed=self.config.random_seed)
  315.             ds_test_obj = Dataset(generator=gen_test, batch_size=self.config.test_batch_size,
  316.                                   buffer_size=self.config.test_batch_size, is_train=False,
  317.                                   len2=input_obj.max_num_bpe_mask)
  318.             self.test_dataset = ds_test_obj.next_element
  319.             if self.gradient_accumulation_steps > 1:
  320.                 self.test_dataset = self.test_dataset.prefetch(buffer_size=8)
  321.  
  322.         start_train_time = time.time()
  323.         # read info from checkpoint file
  324.         if self.ckpt_manager.latest_checkpoint:
  325.             # self.model.load_weights(self.path2model_ckpt)
  326.             self.ckpt.restore(self.ckpt_manager.latest_checkpoint).expect_partial()
  327.             logger_obj.logger.info("Restored from {}".format(self.ckpt_manager.latest_checkpoint))
  328.         else:
  329.             logger_obj.logger.info("Initializing from scratch.")
  330.         # read meta info
  331.         sum_acc_train = 0
  332.         start_epoch, sum_loss_train, count_train = read_meta_data_csv(meta_csv=self.meta_csv)
  333.         i = (self.train_steps_per_epoch * start_epoch) + 1
  334.         self.logger.info("Start process of training...")
  335.         # train operation
  336.         for epoch in range(start_epoch, self.config.num_epochs):
  337.             #pbar = tqdm(total=self.train_steps_per_epoch)
  338.             self.logger.info(f"Start epoch {epoch}")
  339.             for (train_batch, (inputs)) in enumerate(self.train_dataset):
  340.                 #pbar.set_description(f"Current batch #{train_batch+1}")
  341.                 self.logger.info(f"Current batch #{train_batch+1}")
  342.                 learning_rate = self.optimizer.learning_rate(step=tf.constant(i, dtype=tf.float32))
  343.                 loss_scale = self.optimizer.loss_scale()
  344.                 #mlm_loss, mlm_acc, grad_norm, weight_norm = self.train_step(inputs)
  345.                 mlm_loss, mlm_acc = self.train_step(inputs)
  346.                 loss = mlm_loss.numpy()
  347.                 acc = mlm_acc.numpy()
  348.                 # define inputs of model to save later
  349.                 if train_batch % self.config.show_per_step == 0:
  350.                     description = f"Epoch:{epoch+1} | Loss:{mlm_loss:.4f} | Accuracy:{mlm_acc:.4f}"
  351.                     #pbar.set_description(description)
  352.                     self.logger.info(description)
  353.                 #pbar.update(1)
  354.                 sum_loss_train += loss
  355.                 sum_acc_train += acc
  356.                 df_train_sum = pd.DataFrame({'epoch_num' : [epoch], 'loss': [loss], 'acc' : [acc]})
  357.                 df_train_sum.to_csv(self.meta_train_sum, mode='a', sep='\t', index=False, columns=None)
  358.  
  359.                 train_metrics = {
  360.                     #"weight_norm": weight_norm,
  361.                     #"grad_norm": grad_norm,
  362.                     "loss_scale": loss_scale,
  363.                     "learning_rate": learning_rate,
  364.                     "train/mlm_loss": mlm_loss,
  365.                     "train/mlm_acc": mlm_acc,
  366.                 }
  367.                 all_metrics = {**train_metrics}
  368.                 is_final_step = train_batch >= self.train_steps_per_epoch
  369.                 i += 1
  370.                 count_train += 1
  371.                 if is_final_step:
  372.                     break
  373.             av_loss_train = sum_loss_train / count_train
  374.             av_acc_train = sum_acc_train / count_train
  375.             logger_obj.logger.info(f'Train --> Epoch {epoch+1} | Av Loss {av_loss_train:.4f} | Av Acc {av_acc_train:.4f}')
  376.             write_meta_data_csv(epoch=epoch, sum_loss_train=sum_loss_train, count_train=count_train,
  377.                                 meta_csv=self.meta_csv)
  378.             end_train_epoch_time = time.time()
  379.             logger_obj.logger.info(f'Time taken for train 1 epoch: {end_train_epoch_time - start_train_time} secs\n')
  380.             ckpt_save_path = self.ckpt_manager.save()
  381.             logger_obj.logger.info(f'Saving checkpoint for epoch {epoch} to {ckpt_save_path}\n')
  382.             if input_obj.mode == "train_eval":
  383.                 val_mlm_loss, val_mlm_acc = self.test_op(num_epoch=epoch)
  384.                 val_metrics = {"val/mlm_loss": val_mlm_loss,
  385.                                "val/mlm_acc": val_mlm_acc,
  386.                                }
  387.                 all_metrics = {**all_metrics, **val_metrics}
  388.             with self.summary_writer.as_default():
  389.                 for name, val in all_metrics.items():
  390.                     tf.summary.scalar(name, val, step=epoch)
  391.             sum_loss_train = 0.0
  392.             sum_acc_train = 0.0
  393.             count_train = 0
  394.         self.logger.info('End of process of training!')
  395.  
  396. def main_train():
  397.     # Users should always run this script under TF 2.x
  398.     assert tf.version.VERSION.startswith('2.')
  399.     tf.random.set_seed(12345)
  400.     tf.autograph.set_verbosity(0)
  401.     #
  402.     #
  403.     model_name = 'albert_pretrained'
  404.     path2cfgs = fdir_obj.path2maindir + '/models/{}/configs/'.format(model_name)
  405.     if not Path(path2cfgs).is_dir():
  406.         Path(path2cfgs).mkdir(parents=True, exist_ok=True)
  407.     if len(os.listdir(path2cfgs)) == 0:
  408.         for filename in glob.glob(os.path.join(fdir_obj.path2cfgs, '*.*')):
  409.             shutil.copy(filename, path2cfgs)
  410.     nn_gin_cfg = path2cfgs + 'nn.gin'
  411.     train_gin_cfg = path2cfgs + 'train.gin'
  412.     #
  413.     gin.parse_config_file(nn_gin_cfg)
  414.     gin.parse_config_file(train_gin_cfg)
  415.  
  416.     albert_config = NNConfig()
  417.     train_config = TrainConfig()
  418.     #
  419.     # XLA, AutoGraph
  420.     #tf.config.optimizer.set_jit(True)
  421.     #tf.config.experimental_run_functions_eagerly(False)
  422.     # Horovod init
  423.     hvd.init()
  424.     gpus = tf.config.list_physical_devices("GPU")
  425.     print(gpus)
  426.     # set mem growth for nvidia cuda
  427.     for gpu in gpus:
  428.         tf.config.experimental.set_memory_growth(gpu, True)
  429.     if gpus:
  430.         tf.config.set_visible_devices(gpus[hvd.local_rank()], "GPU")
  431.     trainer_obj = PreTrainAlbertLM(model_name=model_name, albert_config=albert_config, train_config=train_config,
  432.                                    load_optimizer=True)
  433.     trainer_obj.train_op()
  434.     #trainer_obj.eval_op()
  435.  
  436.  
  437. if __name__ == "__main__":
  438.     #set_memory_growth()
  439.     #app.run(main_train)
  440.     main_train()
Advertisement
Add Comment
Please, Sign In to add comment