# -------------------------------------------------------- # Image as a Foreign Language: BEiT Pretraining for Vision and Vision-Language Tasks (https://arxiv.org/abs/2208.10442) # Github source: https://github.com/microsoft/unilm/tree/master/beit3 # Copyright (c) 2023 Microsoft # Licensed under The MIT License [see LICENSE for details] # --------------------------------------------------------' import argparse import datetime import io import json import math import os import time from collections import defaultdict, deque from pathlib import Path import numpy as np import torch import torch.distributed as dist import torch.nn as nn import torch.nn.functional as F from timm.utils import get_state_dict from torch import inf def bool_flag(s): """ Parse boolean arguments from the command line. """ FALSY_STRINGS = {"off", "false", "0"} TRUTHY_STRINGS = {"on", "true", "1"} if s.lower() in FALSY_STRINGS: return False elif s.lower() in TRUTHY_STRINGS: return True else: raise argparse.ArgumentTypeError("invalid value for a boolean flag") class SmoothedValue: """Track a series of values and provide access to smoothed values over a window or the global series average. """ def __init__(self, window_size=20, fmt=None): if fmt is None: fmt = "{median:.4f} ({global_avg:.4f})" self.deque = deque(maxlen=window_size) self.total = 0.0 self.count = 0 self.fmt = fmt def update(self, value, n=1): self.deque.append(value) self.count += n self.total += value * n def synchronize_between_processes(self): """ Warning: does not synchronize the deque! """ if not is_dist_avail_and_initialized(): return t = torch.tensor([self.count, self.total], dtype=torch.float64, device='cuda') dist.barrier() dist.all_reduce(t) t = t.tolist() self.count = int(t[0]) self.total = t[1] @property def median(self): d = torch.tensor(list(self.deque)) return d.median().item() @property def avg(self): d = torch.tensor(list(self.deque), dtype=torch.float32) return d.mean().item() @property def global_avg(self): return self.total / self.count @property def max(self): return max(self.deque) @property def value(self): return self.deque[-1] def __str__(self): return self.fmt.format( median=self.median, avg=self.avg, global_avg=self.global_avg, max=self.max, value=self.value, ) class MetricLogger: def __init__(self, delimiter="\t"): self.meters = defaultdict(SmoothedValue) self.delimiter = delimiter def update(self, **kwargs): for k, v in kwargs.items(): if v is None: continue if isinstance(v, torch.Tensor): v = v.item() assert isinstance(v, (float, int)) self.meters[k].update(v) def __getattr__(self, attr): if attr in self.meters: return self.meters[attr] if attr in self.__dict__: return self.__dict__[attr] raise AttributeError(f"'{type(self).__name__}' object has no attribute '{attr}'") def __str__(self): loss_str = [] for name, meter in self.meters.items(): loss_str.append(f"{name}: {str(meter)}") return self.delimiter.join(loss_str) def synchronize_between_processes(self): for meter in self.meters.values(): meter.synchronize_between_processes() def add_meter(self, name, meter): self.meters[name] = meter def log_every(self, iterable, print_freq, header=None): i = 0 if not header: header = '' start_time = time.time() end = time.time() iter_time = SmoothedValue(fmt='{avg:.4f}') data_time = SmoothedValue(fmt='{avg:.4f}') space_fmt = ':' + str(len(str(len(iterable)))) + 'd' log_msg = [ header, '[{0' + space_fmt + '}/{1}]', 'eta: {eta}', '{meters}', 'time: {time}', 'data: {data}', ] if torch.cuda.is_available(): log_msg.append('max mem: {memory:.0f}') log_msg = self.delimiter.join(log_msg) MB = 1024.0 * 1024.0 for obj in iterable: data_time.update(time.time() - end) yield obj iter_time.update(time.time() - end) if i % print_freq == 0 or i == len(iterable) - 1: eta_seconds = iter_time.global_avg * (len(iterable) - i) eta_string = str(datetime.timedelta(seconds=int(eta_seconds))) if torch.cuda.is_available(): print( log_msg.format( i, len(iterable), eta=eta_string, meters=str(self), time=str(iter_time), data=str(data_time), memory=torch.cuda.max_memory_allocated() / MB, ) ) else: print( log_msg.format( i, len(iterable), eta=eta_string, meters=str(self), time=str(iter_time), data=str(data_time), ) ) i += 1 end = time.time() total_time = time.time() - start_time total_time_str = str(datetime.timedelta(seconds=int(total_time))) print( '{} Total time: {} ({:.4f} s / it)'.format( header, total_time_str, total_time / len(iterable) ) ) def _load_checkpoint_for_ema(model_ema, checkpoint): """ Workaround for ModelEma._load_checkpoint to accept an already-loaded object """ mem_file = io.BytesIO() torch.save(checkpoint, mem_file) mem_file.seek(0) model_ema._load_checkpoint(mem_file) def setup_for_distributed(is_master): """ This function disables printing when not in master process """ import builtins as __builtin__ builtin_print = __builtin__.print def print(*args, **kwargs): force = kwargs.pop('force', False) if is_master or force: builtin_print(*args, **kwargs) __builtin__.print = print def is_dist_avail_and_initialized(): if not dist.is_available(): return False if not dist.is_initialized(): return False return True def get_world_size(): if not is_dist_avail_and_initialized(): return 1 return dist.get_world_size() def get_rank(): if not is_dist_avail_and_initialized(): return 0 return dist.get_rank() def is_main_process(): return get_rank() == 0 def save_on_master(*args, **kwargs): if is_main_process(): torch.save(*args, **kwargs) def _get_rank_env(): if "RANK" in os.environ: return int(os.environ["RANK"]) else: return int(os.environ['OMPI_COMM_WORLD_RANK']) def _get_local_rank_env(): if "LOCAL_RANK" in os.environ: return int(os.environ["LOCAL_RANK"]) else: return int(os.environ['OMPI_COMM_WORLD_LOCAL_RANK']) def _get_world_size_env(): if "WORLD_SIZE" in os.environ: return int(os.environ["WORLD_SIZE"]) else: return int(os.environ['OMPI_COMM_WORLD_SIZE']) # The implementation code is modified from DeiT (https://github.com/facebookresearch/deit.git) def init_distributed_mode(args): if args.dist_on_itp: args.rank = _get_rank_env() args.world_size = _get_world_size_env() # int(os.environ['OMPI_COMM_WORLD_SIZE']) args.gpu = _get_local_rank_env() args.dist_url = "tcp://{}:{}".format(os.environ['MASTER_ADDR'], os.environ['MASTER_PORT']) os.environ['LOCAL_RANK'] = str(args.gpu) os.environ['RANK'] = str(args.rank) os.environ['WORLD_SIZE'] = str(args.world_size) # ["RANK", "WORLD_SIZE", "MASTER_ADDR", "MASTER_PORT", "LOCAL_RANK"] elif 'RANK' in os.environ and 'WORLD_SIZE' in os.environ: args.rank = int(os.environ["RANK"]) args.world_size = int(os.environ['WORLD_SIZE']) args.gpu = int(os.environ['LOCAL_RANK']) elif 'SLURM_PROCID' in os.environ: args.rank = int(os.environ['SLURM_PROCID']) args.gpu = args.rank % torch.cuda.device_count() else: print('Not using distributed mode') args.distributed = False return args.distributed = True torch.cuda.set_device(args.gpu) args.dist_backend = 'nccl' print( f'| distributed init (rank {args.rank}): {args.dist_url}, gpu {args.gpu}', flush=True, ) torch.distributed.init_process_group( backend=args.dist_backend, init_method=args.dist_url, world_size=args.world_size, rank=args.rank, timeout=datetime.timedelta(0, 7200), ) torch.distributed.barrier() setup_for_distributed(args.rank == 0) def load_state_dict(model, state_dict, prefix='', ignore_missing="relative_position_index"): missing_keys = [] unexpected_keys = [] error_msgs = [] # copy state_dict so _load_from_state_dict can modify it metadata = getattr(state_dict, '_metadata', None) state_dict = state_dict.copy() if metadata is not None: state_dict._metadata = metadata def load(module, prefix=''): local_metadata = {} if metadata is None else metadata.get(prefix[:-1], {}) module._load_from_state_dict( state_dict, prefix, local_metadata, True, missing_keys, unexpected_keys, error_msgs ) for name, child in module._modules.items(): if child is not None: load(child, prefix + name + '.') load(model, prefix=prefix) warn_missing_keys = [] ignore_missing_keys = [] for key in missing_keys: keep_flag = True for ignore_key in ignore_missing.split('|'): if ignore_key in key: keep_flag = False break if keep_flag: warn_missing_keys.append(key) else: ignore_missing_keys.append(key) missing_keys = warn_missing_keys if len(missing_keys) > 0: print( "Weights of {} not initialized from pretrained model: {}".format( model.__class__.__name__, missing_keys ) ) if len(unexpected_keys) > 0: print( "Weights from pretrained model not used in {}: {}".format( model.__class__.__name__, unexpected_keys ) ) if len(ignore_missing_keys) > 0: print( "Ignored weights of {} not initialized from pretrained model: {}".format( model.__class__.__name__, ignore_missing_keys ) ) if len(error_msgs) > 0: print('\n'.join(error_msgs)) class NativeScalerWithGradNormCount: state_dict_key = "amp_scaler" def __init__(self): self._scaler = torch.cuda.amp.GradScaler() def __call__( self, loss, optimizer, clip_grad=None, parameters=None, create_graph=False, update_grad=True ): self._scaler.scale(loss).backward(create_graph=create_graph) if update_grad: if clip_grad is not None: assert parameters is not None self._scaler.unscale_( optimizer ) # unscale the gradients of optimizer's assigned params in-place norm = torch.nn.utils.clip_grad_norm_(parameters, clip_grad) else: self._scaler.unscale_(optimizer) norm = get_grad_norm_(parameters) self._scaler.step(optimizer) self._scaler.update() else: norm = None return norm def state_dict(self): return self._scaler.state_dict() def load_state_dict(self, state_dict): self._scaler.load_state_dict(state_dict) def get_grad_norm_(parameters, norm_type: float = 2.0) -> torch.Tensor: if isinstance(parameters, torch.Tensor): parameters = [parameters] parameters = [p for p in parameters if p.grad is not None] norm_type = float(norm_type) if len(parameters) == 0: return torch.tensor(0.0) device = parameters[0].grad.device if norm_type == inf: total_norm = max(p.grad.detach().abs().max().to(device) for p in parameters) else: total_norm = torch.norm( torch.stack([torch.norm(p.grad.detach(), norm_type).to(device) for p in parameters]), norm_type, ) return total_norm def cosine_scheduler( base_value, final_value, epochs, niter_per_ep, warmup_epochs=0, start_warmup_value=0, warmup_steps=-1, sched_type="cos", ): warmup_schedule = np.array([]) warmup_iters = warmup_epochs * niter_per_ep if warmup_steps > 0: warmup_iters = warmup_steps print("Set warmup steps = %d" % warmup_iters) if warmup_epochs > 0: warmup_schedule = np.linspace(start_warmup_value, base_value, warmup_iters) if sched_type == "cos": iters = np.arange(epochs * niter_per_ep - warmup_iters) schedule = np.array( [ final_value + 0.5 * (base_value - final_value) * (1 + math.cos(math.pi * i / (len(iters)))) for i in iters ] ) elif sched_type == "linear": schedule = np.linspace(base_value, final_value, epochs * niter_per_ep - warmup_iters) else: raise NotImplementedError() schedule = np.concatenate((warmup_schedule, schedule)) assert len(schedule) == epochs * niter_per_ep return schedule def save_model(args, epoch, model, model_without_ddp, optimizer, loss_scaler, model_ema=None): output_dir = Path(args.output_dir) if loss_scaler is not None: checkpoint_paths = [output_dir / ('checkpoint-%s.pth' % epoch)] for checkpoint_path in checkpoint_paths: to_save = { 'model': model_without_ddp.state_dict(), 'optimizer': optimizer.state_dict(), 'epoch': epoch, 'scaler': loss_scaler.state_dict(), 'args': args, } if model_ema is not None: to_save['model_ema'] = get_state_dict(model_ema) save_on_master(to_save, checkpoint_path) else: client_state = {'epoch': epoch, "args": args} if model_ema is not None: client_state['model_ema'] = get_state_dict(model_ema) model.save_checkpoint( save_dir=args.output_dir, tag="checkpoint-%s" % epoch, client_state=client_state ) def auto_load_model(args, model, model_without_ddp, optimizer, loss_scaler, model_ema=None): output_dir = Path(args.output_dir) if loss_scaler is not None: # torch.amp if args.auto_resume and len(args.resume) == 0: import glob all_checkpoints = glob.glob(os.path.join(output_dir, 'checkpoint-*.pth')) latest_ckpt = -1 for ckpt in all_checkpoints: t = ckpt.split('-')[-1].split('.')[0] if t.isdigit(): latest_ckpt = max(int(t), latest_ckpt) if latest_ckpt >= 0: args.resume = os.path.join(output_dir, 'checkpoint-%d.pth' % latest_ckpt) print("Auto resume checkpoint: %s" % args.resume) if args.resume: if args.resume.startswith('https'): checkpoint = torch.hub.load_state_dict_from_url( args.resume, map_location='cpu', check_hash=True ) else: checkpoint = torch.load(args.resume, map_location='cpu') model_without_ddp.load_state_dict(checkpoint['model']) print("Resume checkpoint %s" % args.resume) if 'optimizer' in checkpoint and 'epoch' in checkpoint: optimizer.load_state_dict(checkpoint['optimizer']) args.start_epoch = checkpoint['epoch'] + 1 if hasattr(args, 'model_ema') and args.model_ema: _load_checkpoint_for_ema(model_ema, checkpoint['model_ema']) if 'scaler' in checkpoint: loss_scaler.load_state_dict(checkpoint['scaler']) print("With optim & sched!") else: # deepspeed, only support '--auto_resume'. if args.auto_resume: import glob all_checkpoints = glob.glob(os.path.join(output_dir, 'checkpoint-*')) latest_ckpt = -1 for ckpt in all_checkpoints: t = ckpt.split('-')[-1].split('.')[0] if t.isdigit(): latest_ckpt = max(int(t), latest_ckpt) if latest_ckpt >= 0: args.resume = os.path.join(output_dir, 'checkpoint-%d' % latest_ckpt) print("Auto resume checkpoint: %d" % latest_ckpt) _, client_states = model.load_checkpoint( args.output_dir, tag='checkpoint-%d' % latest_ckpt ) args.start_epoch = client_states['epoch'] + 1 if model_ema is not None: if args.model_ema: _load_checkpoint_for_ema(model_ema, client_states['model_ema']) # The implementation code is modified from DeiT (https://github.com/facebookresearch/deit.git) def load_model_and_may_interpolate(ckpt_path, model, model_key, model_prefix): if ckpt_path.startswith('https'): checkpoint = torch.hub.load_state_dict_from_url( ckpt_path, map_location='cpu', check_hash=True ) else: checkpoint = torch.load(ckpt_path, map_location='cpu') print("Load ckpt from %s" % ckpt_path) checkpoint_model = None for model_key in model_key.split('|'): if model_key in checkpoint: checkpoint_model = checkpoint[model_key] print("Load state_dict by model_key = %s" % model_key) break if checkpoint_model is None: checkpoint_model = checkpoint state_dict = model.state_dict() for k in ['head.weight', 'head.bias']: if k in checkpoint_model and checkpoint_model[k].shape != state_dict[k].shape: print(f"Removing key {k} from pretrained checkpoint") del checkpoint_model[k] # interpolate position embedding for pos_embed_key in ( "vision_pos_embed", "pos_embed", "beit3.encoder.embed_positions.A.weight", ): if pos_embed_key in checkpoint_model: pos_embed_checkpoint = checkpoint_model[pos_embed_key] embedding_size = pos_embed_checkpoint.shape[-1] if pos_embed_key == "beit3.encoder.embed_positions.A.weight": # being consistent with Fairseq, which starts from 2 for position embedding torchscale_model = True num_patches = model.beit3.vision_embed.num_patches num_extra_tokens = ( model.beit3.vision_embed.num_position_embeddings() + 2 - num_patches ) else: torchscale_model = False num_patches = model.patch_embed.num_patches num_extra_tokens = getattr(model, pos_embed_key).shape[-2] - num_patches # height (== width) for the checkpoint position embedding orig_size = int((pos_embed_checkpoint.shape[-2] - num_extra_tokens) ** 0.5) # height (== width) for the new position embedding new_size = int(num_patches**0.5) # class_token and dist_token are kept unchanged if orig_size != new_size: print( "Position interpolate from %dx%d to %dx%d" % (orig_size, orig_size, new_size, new_size) ) if torchscale_model: extra_tokens = pos_embed_checkpoint[:num_extra_tokens].unsqueeze(0) # only the position tokens are interpolated pos_tokens = pos_embed_checkpoint[num_extra_tokens:] else: extra_tokens = pos_embed_checkpoint[:, :num_extra_tokens] # only the position tokens are interpolated pos_tokens = pos_embed_checkpoint[:, num_extra_tokens:] pos_tokens = pos_tokens.reshape(-1, orig_size, orig_size, embedding_size).permute( 0, 3, 1, 2 ) pos_tokens = torch.nn.functional.interpolate( pos_tokens, size=(new_size, new_size), mode='bicubic', align_corners=False ) pos_tokens = pos_tokens.permute(0, 2, 3, 1).flatten(1, 2) new_pos_embed = torch.cat((extra_tokens, pos_tokens), dim=1) if torchscale_model: new_pos_embed = new_pos_embed.squeeze(0) checkpoint_model[pos_embed_key] = new_pos_embed load_state_dict(model, checkpoint_model, prefix=model_prefix) def create_ds_config(args): args.deepspeed_config = os.path.join(args.output_dir, "deepspeed_config.json") with open(args.deepspeed_config, mode="w") as writer: ds_config = { "train_batch_size": args.batch_size * args.update_freq * get_world_size(), "train_micro_batch_size_per_gpu": args.batch_size, "steps_per_print": 1000, "optimizer": { "type": "Adam", "adam_w_mode": True, "params": { "lr": args.lr, "weight_decay": args.weight_decay, "bias_correction": True, "betas": [args.opt_betas[0], args.opt_betas[1]], "eps": args.opt_eps, }, }, "fp16": { "enabled": True, "loss_scale": 0, "initial_scale_power": getattr(args, "initial_scale_power", 12), "loss_scale_window": 1000, "hysteresis": 2, "min_loss_scale": 1, }, "amp": {"enabled": False, "opt_level": "O2"}, } if args.clip_grad is not None: ds_config.update({'gradient_clipping': args.clip_grad}) if args.zero_stage == 1: ds_config.update( {"zero_optimization": {"stage": args.zero_stage, "reduce_bucket_size": 5e8}} ) elif args.zero_stage > 1: raise NotImplementedError() writer.write(json.dumps(ds_config, indent=2)) def merge_batch_tensors_by_dict_key(batch): batch_tensors = {} for tensor_key in batch[0]: if isinstance(batch[0][tensor_key], torch.Tensor): batch_tensors[tensor_key] = torch.stack([d[tensor_key] for d in batch]) else: batch_tensors[tensor_key] = torch.tensor( [d[tensor_key] for d in batch], dtype=torch.long ) return batch_tensors def get_loss_scale_for_deepspeed(model): optimizer = model.optimizer loss_scale = None if hasattr(optimizer, 'loss_scale'): loss_scale = optimizer.loss_scale elif hasattr(optimizer, 'cur_scale'): loss_scale = optimizer.cur_scale return loss_scale class GatherLayer(torch.autograd.Function): """ Gather tensors from all workers with support for backward propagation: This implementation does not cut the gradients as torch.distributed.all_gather does. """ @staticmethod def forward(ctx, x): output = [torch.zeros_like(x) for _ in range(dist.get_world_size())] dist.all_gather(output, x) return tuple(output) @staticmethod def backward(ctx, *grads): all_gradients = torch.stack(grads) dist.all_reduce(all_gradients) return all_gradients[dist.get_rank()] def gather_features( image_features, text_features, ): gathered_image_features = GatherLayer.apply(image_features) gathered_text_features = GatherLayer.apply(text_features) all_image_features = torch.cat(gathered_image_features) all_text_features = torch.cat(gathered_text_features) return all_image_features, all_text_features # The implementation code is modified from open_clip (https://github.com/mlfoundations/open_clip.git) class ClipLoss(nn.Module): def __init__( self, cache_labels=False, rank=0, world_size=1, ): super().__init__() self.cache_labels = cache_labels self.rank = rank self.world_size = world_size # cache state self.prev_num_logits = 0 self.labels = {} def forward(self, image_features, text_features, logit_scale): device = image_features.device if self.world_size > 1: all_image_features, all_text_features = gather_features(image_features, text_features) logits_per_image = logit_scale * image_features @ all_text_features.T logits_per_text = logit_scale * text_features @ all_image_features.T else: logits_per_image = logit_scale * image_features @ text_features.T logits_per_text = logit_scale * text_features @ image_features.T # calculated ground-truth and cache if enabled num_logits = logits_per_image.shape[0] if self.prev_num_logits != num_logits or device not in self.labels: labels = torch.arange(num_logits, device=device, dtype=torch.long) if self.world_size > 1: labels = labels + num_logits * self.rank if self.cache_labels: self.labels[device] = labels self.prev_num_logits = num_logits else: labels = self.labels[device] total_loss = ( F.cross_entropy(logits_per_image, labels) + F.cross_entropy(logits_per_text, labels) ) / 2 return total_loss, logits_per_image, logits_per_text def write_result_to_jsonl(test_stats, result_file): with open(result_file, mode="w", encoding="utf-8") as writer: writer.write(json.dumps(test_stats, indent=None)) def read_result_from_jsonl(result_file): with open(result_file, encoding="utf-8") as reader: return json.load(reader) class BertCaptioningLoss(nn.Module): def __init__(self, label_smoothing, drop_worst_ratio, drop_worst_after): super().__init__() self.label_smoothing = label_smoothing self.drop_worst_ratio = drop_worst_ratio self.drop_worst_after = drop_worst_after self.log_soft = nn.LogSoftmax(dim=1) self.kl = nn.KLDivLoss(reduction='none') self.iter = 0 def forward(self, logits, target, iter): eps = self.label_smoothing n_class = logits.size(1) one_hot = torch.zeros_like(logits).scatter(1, target.view(-1, 1), 1) one_hot = one_hot * (1 - eps) + (1 - one_hot) * eps / (n_class - 1) log_prb = self.log_soft(logits) loss = self.kl(log_prb, one_hot).sum(1) if self.drop_worst_ratio > 0 and iter > self.drop_worst_after: loss, _ = torch.topk( loss, k=int(loss.shape[0] * (1 - self.drop_worst_ratio)), largest=False ) loss = loss.mean() return loss class BeamHypotheses: def __init__(self, n_hyp, max_length, length_penalty, early_stopping): """ Initialize n-best list of hypotheses. """ self.max_length = max_length - 1 # ignoring bos_token self.length_penalty = length_penalty self.early_stopping = early_stopping self.n_hyp = n_hyp self.hyp = [] self.worst_score = 1e9 def __len__(self): """ Number of hypotheses in the list. """ return len(self.hyp) def add(self, hyp, sum_logprobs): """ Add a new hypothesis to the list. """ score = sum_logprobs / len(hyp) ** self.length_penalty if len(self) < self.n_hyp or score > self.worst_score: self.hyp.append((score, hyp)) if len(self) > self.n_hyp: sorted_scores = sorted([(s, idx) for idx, (s, _) in enumerate(self.hyp)]) del self.hyp[sorted_scores[0][1]] self.worst_score = sorted_scores[1][0] else: self.worst_score = min(score, self.worst_score) def is_done(self, best_sum_logprobs): """ If there are enough hypotheses and that none of the hypotheses being generated can become better than the worst one in the heap, then we are done with this sentence. """ if len(self) < self.n_hyp: return False elif self.early_stopping: return True else: return self.worst_score >= best_sum_logprobs / self.max_length**self.length_penalty def dump_predictions(args, result, file_suffix): global_rank = get_rank() jsons = None if global_rank >= 0: output_file = os.path.join(args.task_cache_path, f"submit_{global_rank}_{file_suffix}.json") with open(output_file, "w") as fp: json.dump(result, fp, indent=2) torch.distributed.barrier() if global_rank == 0: world_size = get_world_size() jsons = [] for i in range(world_size): each_file = os.path.join(args.task_cache_path, f"submit_{i}_{file_suffix}.json") with open(each_file) as fp: jsons += json.load(fp) new_jsons = [] res_dict = dict() if args.task in ["coco_captioning", "nocaps"]: qid_key = "image_id" else: # for VQAv2 qid_key = "question_id" for item in jsons: if item[qid_key] in res_dict: continue new_jsons.append(item) res_dict[item[qid_key]] = item jsons = new_jsons torch.distributed.barrier() os.remove(output_file) else: jsons = result result_file = os.path.join(args.output_dir, f"submit_{file_suffix}.json") if jsons is not None: with open(result_file, "w") as fp: json.dump(jsons, fp, indent=2) print("Infer %d examples into %s" % (len(jsons), result_file)) return result_file